Repository: grapl-security/grapl (language: python)

Issue:
Merge main into nomad (so we can then merge nomad into main)
Merge main into nomad

AlsoI've introduced something New And Cool in `integration-tests.nomad` - we now run python on the host OS, no docker or anything, via pants.

# HEY REVIEWERS
So, a lot of this stuff is jsut from the merge, but there's a few important changes of my own peppered throughout. 
Just ctrl-f for `actual-new-change-from-this-pr` to see my comments about them.


Discussion / hints:
This pull request **introduces 2 alerts** and **fixes 2** when merging 04888176aa387b3c2b5dda10eb5d8116979a0f75 into ea0977592b3fc521ba26db0cef299637ac4d8e00 - [view on LGTM.com](https://lgtm.com/projects/g/grapl-security/grapl/rev/pr-402e3cbbb21bb12e114b05ee64e6c73f911b5743)

**new alerts:**

* 2 for Unused import

**fixed alerts:**

* 2 for Unused local variable
This pull request **introduces 2 alerts** and **fixes 2** when merging 1c5b6759e270e95d699e21d84f0a178ba8e67a05 into ea0977592b3fc521ba26db0cef299637ac4d8e00 - [view on LGTM.com](https://lgtm.com/projects/g/grapl-security/grapl/rev/pr-37a7725c838cbf330c9991d3a49141b4e214d0af)

**new alerts:**

* 2 for Unused import

**fixed alerts:**

* 2 for Unused local variable
This pull request **introduces 2 alerts** and **fixes 2** when merging d22460821cf0198adedf86c5e0258f8094cfc922 into 4f4cd3936b3e21651b02c1475077eb1818e6e2f5 - [view on LGTM.com](https://lgtm.com/projects/g/grapl-security/grapl/rev/pr-4ec7f0ed020cf172ca9bcf01b9bf499828c468d3)

**new alerts:**

* 2 for Unused import

**fixed alerts:**

* 2 for Unused local variable
This pull request **introduces 1 alert** and **fixes 2** when merging 55069f7b601841192ca953952be2d739b8eefc80 into 4f4cd3936b3e21651b02c1475077eb1818e6e2f5 - [view on LGTM.com](https://lgtm.com/projects/g/grapl-security/grapl/rev/pr-e024a13a0b17e30c90ce2c1d564df2df196bb139)

**new alerts:**

* 1 for Unused import

**fixed alerts:**

* 2 for Unused local variable
This pull request **introduces 1 alert** and **fixes 2** when merging 1b58918bbdf07242f8d8b54000c17c9e3d4d3c0a into 4f4cd3936b3e21651b02c1475077eb1818e6e2f5 - [view on LGTM.com](https://lgtm.com/projects/g/grapl-security/grapl/rev/pr-1968b21064d5129c1d8c5fd974fe57d5a9b4061c)

**new alerts:**

* 1 for Unused import

**fixed alerts:**

* 2 for Unused local variable
This pull request **introduces 1 alert** and **fixes 2** when merging c25fa08dadc4837419322e6ed4e140286bb0014d into 4f4cd3936b3e21651b02c1475077eb1818e6e2f5 - [view on LGTM.com](https://lgtm.com/projects/g/grapl-security/grapl/rev/pr-deea715a545b35b15802954461d1857f25d9b411)

**new alerts:**

* 1 for Unused import

**fixed alerts:**

* 2 for Unused local variable
# [Codecov](https://codecov.io/gh/grapl-security/grapl/pull/1185?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=grapl-security) Report
> :exclamation: No coverage uploaded for pull request base (`nomad@4f4cd39`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=grapl-security#section-missing-base-commit).
> The diff coverage is `n/a`.

> :exclamation: Current head c25fa08 differs from pull request most recent head 00aac36. Consider uploading reports for the commit 00aac36 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/grapl-security/grapl/pull/1185/graphs/tree.svg?width=650&height=150&src=pr&token=HlrWgwYOvc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=grapl-security)](https://codecov.io/gh/grapl-security/grapl/pull/1185?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=grapl-security)

```diff
@@           Coverage Diff            @@
##             nomad    #1185   +/-   ##
========================================
  Coverage         ?   34.76%           
========================================
  Files            ?      307           
  Lines            ?    14131           
  Branches         ?       23           
========================================
  Hits             ?     4912           
  Misses           ?     9204           
  Partials         ?       15           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/grapl-security/grapl/pull/1185?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=grapl-security).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=grapl-security)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/grapl-security/grapl/pull/1185?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=grapl-security). Last update [4f4cd39...00aac36](https://codecov.io/gh/grapl-security/grapl/pull/1185?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=grapl-security). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=grapl-security).

This pull request **introduces 1 alert** and **fixes 2** when merging 00aac365220df4662d04ea969a487d8b69e570f5 into 4f4cd3936b3e21651b02c1475077eb1818e6e2f5 - [view on LGTM.com](https://lgtm.com/projects/g/grapl-security/grapl/rev/pr-0bd35421cb130bf9d82b1371b4ab54cdfdcef8ba)

**new alerts:**

* 1 for Unused import

**fixed alerts:**

* 2 for Unused local variable
This pull request **introduces 1 alert** and **fixes 1** when merging 742441556efa32ecf89c4f2b06f9a2262ffb8808 into d16db05e4ef4a4372ef796b2a92db7a7d6afca24 - [view on LGTM.com](https://lgtm.com/projects/g/grapl-security/grapl/rev/pr-f6b9c186dd537defb9e394a968ae6e7be5990073)

**new alerts:**

* 1 for Unused import

**fixed alerts:**

* 1 for Unused local variable
This pull request **introduces 1 alert** and **fixes 1** when merging 4e8e5795b6235018257cff2f39f0ef522464a1a6 into d16db05e4ef4a4372ef796b2a92db7a7d6afca24 - [view on LGTM.com](https://lgtm.com/projects/g/grapl-security/grapl/rev/pr-fcfaa885eb2972b396a28a8191d8ea278be4a264)

**new alerts:**

* 1 for Unused import

**fixed alerts:**

* 1 for Unused local variable

Fix patch:
diff --git a/.buildkite/pipeline.merge.ami-build.yml b/.buildkite/pipeline.merge.ami-build.yml
deleted file mode 100644
index 893c347419..0000000000
--- a/.buildkite/pipeline.merge.ami-build.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-# This file borrows heavily from the pattern defined in `pipeline-infrastructure`.
-
----
-env:
-  PANTS_CONFIG_FILES: "['pants.toml', 'pants.ci.toml']"
-
-# This is defined in a separate pipeline file because
-# chronotc/monorepo-diff does not currently have support for plugin
-# (or key) configuration on commands.
-# :(
-
-steps:
-  - label: ":packer: AMI Build Server"
-    command: ".buildkite/scripts/build_packer_ci.sh"
-    key: "ami-build-server" # Used to tag the Packer Builder instance
-    plugins:
-      - docker#v3.8.0:
-          image: "hashicorp/packer:1.7.2"
-          entrypoint: bash
-          propagate-aws-auth-tokens: true
-          environment:
-            - "BUILDKITE_BRANCH"
-            - "BUILDKITE_BUILD_NUMBER"
-            - "BUILDKITE_COMMIT"
-            - "BUILDKITE_PIPELINE_NAME"
-            - "BUILDKITE_STEP_KEY"
-            - "PACKER_IMAGE_NAME=grapl-nomad-consul-server"
-            # I hate these last two, but it seems to be necessary at
-            # the moment while this runs inside a Docker container.
-            - "BUILDKITE_ARTIFACT_UPLOAD_DESTINATION"
-            - "BUILDKITE_S3_DEFAULT_REGION"
-    agents:
-      queue: "packer-staging"
-
-  - label: ":packer: AMI Build Client"
-    command: ".buildkite/scripts/build_packer_ci.sh"
-    key: "ami-build-client" # Used to tag the Packer Builder instance
-    plugins:
-      - docker#v3.8.0:
-          image: "hashicorp/packer:1.7.2"
-          entrypoint: bash
-          propagate-aws-auth-tokens: true
-          environment:
-            - "BUILDKITE_BRANCH"
-            - "BUILDKITE_BUILD_NUMBER"
-            - "BUILDKITE_COMMIT"
-            - "BUILDKITE_PIPELINE_NAME"
-            - "BUILDKITE_STEP_KEY"
-            - "PACKER_IMAGE_NAME=grapl-nomad-consul-client"
-            # I hate these last two, but it seems to be necessary at
-            # the moment while this runs inside a Docker container.
-            - "BUILDKITE_ARTIFACT_UPLOAD_DESTINATION"
-            - "BUILDKITE_S3_DEFAULT_REGION"
-    agents:
-      queue: "packer-staging"
-
-  - wait
-
-  # This just has to extract the AMI IDs and jam it into the artifact
-  # store to pick up later.
-  - label: ":writing_hand: Record AMI IDs"
-    command:
-      - .buildkite/scripts/record_artifacts.sh
-    artifact_paths:
-      - "*.grapl-artifacts.json"
diff --git a/.buildkite/pipeline.merge.yml b/.buildkite/pipeline.merge.yml
index ba8eff6288..f9f72adb17 100644
--- a/.buildkite/pipeline.merge.yml
+++ b/.buildkite/pipeline.merge.yml
@@ -81,23 +81,19 @@ steps:
     artifact_paths:
       - "*.grapl-artifacts.json"
 
-  - label: ":thinking_face: AMI Build?"
+  - label: ":thinking_face: UX Build?"
     plugins:
       - chronotc/monorepo-diff#v2.0.4:
           diff: .buildkite/shared/scripts/diff.sh
           log_level: "debug"
           watch:
             - path:
-                - "packer/"
-                - ".buildkite/scripts/build_packer_ci.sh"
-                - ".buildkite/scripts/record_artifacts.sh"
-                - ".buildkite/scripts/lib/packer.sh"
-                - ".buildkite/scripts/lib/packer_constants.sh"
-                - ".buildkite/pipeline.merge.ami-build.yml"
+                - "src/js/engagement_view/"
+                - ".buildkite/scripts/upload_ux_assets.sh"
+                - ".buildkite/pipeline.merge.ux-build.yml"
               config:
-                label: ":pipeline: Upload AMI Build"
-                command: "buildkite-agent pipeline upload .buildkite/pipeline.merge.ami-build.yml"
-
+                label: ":pipeline: Upload UX Build"
+                command: "buildkite-agent pipeline upload .buildkite/pipeline.merge.ux-build.yml"
   - wait
 
   # Downloads and operates on all the *.grapl-artifacts.json files
diff --git a/.buildkite/pipeline.verify.yml b/.buildkite/pipeline.verify.yml
index 3966b49435..d7f2c7ad37 100644
--- a/.buildkite/pipeline.verify.yml
+++ b/.buildkite/pipeline.verify.yml
@@ -68,12 +68,7 @@ steps:
 
   - label: ":python::jeans: Typechecking"
     command:
-      - make test-typecheck-pants
-
-  # TODO: Consider beefy queue
-  - label: ":python::docker: Typechecking (:no_entry_sign::jeans:)"
-    command:
-      - make test-typecheck-docker
+      - make test-typecheck
 
   # TODO: Consider beefy queue
   - label: ":typescript::docker: Unit Tests"
@@ -122,31 +117,14 @@ steps:
       queue: "beefy"
     artifact_paths:
       - "test_artifacts/**/*"
+    retry:
+      automatic:
+        limit: 5
 
-  - label: ":lint-roller::packer::nomad: Lint HCL files"
+  - label: ":lint-roller::nomad: Lint HCL files"
     command:
       - make lint-hcl
 
-  - label: ":thinking_face: AMI Test?"
-    # Don't want to run this again in the merge pipeline, since
-    # building the image does exactly the same thing. We don't want to
-    # waste time.
-    if: build.env("BUILDKITE_PIPELINE_NAME") == "grapl/verify"
-    plugins:
-      - chronotc/monorepo-diff#v2.0.4:
-          diff: .buildkite/shared/scripts/diff.sh
-          log_level: "debug"
-          watch:
-            - path:
-                - ".buildkite/scripts/test_packer.sh"
-                - "packer/"
-                - ".buildkite/scripts/build_packer_ci.sh"
-                - ".buildkite/scripts/lib/packer.sh"
-                - ".buildkite/scripts/lib/packer_constants.sh"
-              config:
-                label: ":pipeline: Upload AMI Test"
-                command: "buildkite-agent pipeline upload .buildkite/pipeline.verify.ami-test.yml"
-
   - label: ":thinking_face::rust: Cargo Audit?"
     plugins:
       - chronotc/monorepo-diff#v2.0.4:
diff --git a/.buildkite/scripts/build_packer_ci.sh b/.buildkite/scripts/build_packer_ci.sh
deleted file mode 100755
index bd40bb3795..0000000000
--- a/.buildkite/scripts/build_packer_ci.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env bash
-
-# TODO: Deduplicate - https://github.com/grapl-security/issue-tracker/issues/614
-
-# Build our AMI in Buildkite, supplying the necessary
-# information to create metadata tags.
-# This is called from .buildkite/pipeline.merge.ami-build.yml
-
-set -euo pipefail
-
-source .buildkite/scripts/lib/aws.sh
-source .buildkite/scripts/lib/packer.sh
-
-export GIT_SHA="${BUILDKITE_COMMIT}"
-export GIT_BRANCH="${BUILDKITE_BRANCH}"
-# This : syntax does nothing; but in unison with `set -u` marks these two vars as required.
-: "${BUILDKITE_BUILD_NUMBER}"
-# it is worried that I confused this with PACKER_IMAGE_NAMES. I didn't.
-# shellcheck disable=SC2153
-: "${PACKER_IMAGE_NAME}"
-
-# As long as this is running inside a Docker container, we have to
-# make sure we pass these in.
-: "${BUILDKITE_ARTIFACT_UPLOAD_DESTINATION}"
-: "${BUILDKITE_S3_DEFAULT_REGION}"
-
-build_packer_ci() {
-    echo -e "--- :packer: Performing build of AMI"
-
-    # Both defined in packer.sh
-    build_ami "${PACKER_IMAGE_NAME}"
-
-    # HACK; see documentation of this function for details
-    unset_aws_variables
-
-    upload_manifest "${PACKER_IMAGE_NAME}"
-}
-
-build_packer_ci
diff --git a/.buildkite/scripts/build_packer_local.sh b/.buildkite/scripts/build_packer_local.sh
deleted file mode 100755
index d14cdc2b9e..0000000000
--- a/.buildkite/scripts/build_packer_local.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-
-# TODO: Deduplicate - https://github.com/grapl-security/issue-tracker/issues/614
-
-# Build our AMI on a local workstation, supplying the necessary
-# information to create metadata tags.
-
-set -euo pipefail
-
-source .buildkite/scripts/lib/packer.sh
-
-GIT_SHA=$(git rev-parse --short HEAD)
-export GIT_SHA
-GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
-export GIT_BRANCH
-
-build_ami "$@"
diff --git a/.buildkite/scripts/lib/BUILD b/.buildkite/scripts/lib/BUILD
index ab34a90b6f..5859044e5a 100644
--- a/.buildkite/scripts/lib/BUILD
+++ b/.buildkite/scripts/lib/BUILD
@@ -1,17 +1,5 @@
 shell_library()
 
-# Make the `.jq` filters available at test time
-files(
-    name="test_fixtures",
-    sources=[
-        "*.jq",
-        "example_packer_artifact.json",
-    ]
-)
-
 shunit2_tests(
-    name="tests",
-    dependencies=[
-        ":test_fixtures",
-    ]
+    name="tests"
 )
diff --git a/.buildkite/scripts/lib/example_packer_artifact.json b/.buildkite/scripts/lib/example_packer_artifact.json
deleted file mode 100644
index 0d9245f643..0000000000
--- a/.buildkite/scripts/lib/example_packer_artifact.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "builds": [
-    {
-      "name": "amazon-linux-2-amd64-ami",
-      "builder_type": "amazon-ebs",
-      "build_time": 1626132569,
-      "files": null,
-      "artifact_id": "us-east-1:ami-111,us-east-2:ami-222,us-west-1:ami-333,us-west-2:ami-444",
-      "packer_run_uuid": "whatever",
-      "custom_data": null
-    }
-  ],
-  "last_run_uuid": "whatever"
-}
\ No newline at end of file
diff --git a/.buildkite/scripts/lib/extract_ami_id_dict.jq b/.buildkite/scripts/lib/extract_ami_id_dict.jq
deleted file mode 100644
index dd87936736..0000000000
--- a/.buildkite/scripts/lib/extract_ami_id_dict.jq
+++ /dev/null
@@ -1,21 +0,0 @@
-# Get the root amis field, which looks like:
-# us-east-1:ami-111,us-east-2:ami-222,us-west-1:ami-333,us-west-2:ami-444
-.builds[-1].artifact_id
-
-# sample output:
-# ["us-east-1:ami-111", ...]
-| split(",")
-
-# array of [region, ami-id]
-# sample output: 
-# [ ["us-east-1": "ami-111"], ...]
-| map(split(":"))
-
-# sample output:
-# { "imgname.us-east-1": "ami-111", ...}
-| reduce .[] as $region_and_ami_id(
-    {};  # becomes the self - or `.` in below context
-    # We use a period delimiter in the key so that, much later down the pipeline,  
-    # we can shove it into `pulumi config set --path artifacts.imagename.region`
-    .[$IMAGE_NAME + "." + $region_and_ami_id[0]] = $region_and_ami_id[1]  # {[k] = v}
-)
\ No newline at end of file
diff --git a/.buildkite/scripts/lib/packer.sh b/.buildkite/scripts/lib/packer.sh
deleted file mode 100644
index e540ed50d0..0000000000
--- a/.buildkite/scripts/lib/packer.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-source .buildkite/scripts/lib/packer_constants.sh
-
-# Localizing the core "logic" (such as it is) for building our AMI. We
-# use this both to build locally and in CI.
-build_ami() {
-    # Example usage:
-    # PACKER_VARS="-var build_ami=false" build_ami grapl-nomad-consul-server
-    # PACKER_VARS="-var build_ami=false" build_ami grapl-nomad-consul-client
-
-    local -r packer_image_name="$1"
-
-    # shellcheck disable=SC2086
-    packer build ${PACKER_VARS:-} -var "image_name=${packer_image_name}" packer/nomad-server-client
-}
-
-upload_manifest() {
-    # Given a packer image name, upload its manifest to Buildkite
-    local -r packer_image_name="$1"
-    local -r manifest="${packer_image_name}${PACKER_MANIFEST_SUFFIX}"
-
-    echo -e "--- :packer: Manifest ${manifest} Contents"
-    cat "${manifest}"
-    echo
-
-    echo -e "--- :buildkite: Uploading ${manifest} file"
-    buildkite-agent artifact upload "${manifest}"
-    # This artifact then gets picked up by the "Record AMI IDs" step in Buildkite
-
-    # Just to be safe, because subsequent runs can append to it
-    rm "${manifest}"
-}
diff --git a/.buildkite/scripts/lib/packer_constants.sh b/.buildkite/scripts/lib/packer_constants.sh
deleted file mode 100755
index 1f7c69d99c..0000000000
--- a/.buildkite/scripts/lib/packer_constants.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env bash
-# shellcheck disable=SC2034
-
-# This file contains various constants that are used in more than one
-# place in our pipeline scripts.
-#
-# They should all be marked as `readonly`, be named in
-# SCREAMING_SNAKE_CASE, and ordered alphabetically when possible.
-########################################################################
-
-# These are specified in `local.image_name`
-readonly PACKER_IMAGE_NAME_SERVER="grapl-nomad-consul-server"
-readonly PACKER_IMAGE_NAME_CLIENT="grapl-nomad-consul-client"
-readonly PACKER_IMAGE_NAMES=(
-    "${PACKER_IMAGE_NAME_SERVER}"
-    "${PACKER_IMAGE_NAME_CLIENT}"
-)
-
-readonly PACKER_MANIFEST_SUFFIX=".packer-manifest.json"
diff --git a/.buildkite/scripts/record_artifacts.sh b/.buildkite/scripts/record_artifacts.sh
deleted file mode 100755
index 001b8f9f91..0000000000
--- a/.buildkite/scripts/record_artifacts.sh
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-source .buildkite/scripts/lib/artifacts.sh
-source .buildkite/scripts/lib/packer_constants.sh
-readonly jq_filter_path=".buildkite/scripts/lib/extract_ami_id_dict.jq"
-
-generate_artifacts_file() {
-    # Takes in the name of an image, like "image_name", and expects to find
-    # a corresponding packer manifest names "image_name.packer-manifest.json".
-
-    # e.g. "grapl-service"
-    local -r packer_image_name=$1
-    # e.g. "grapl-service.packer-manifest.json"
-    local -r manifest_file="${packer_image_name}${PACKER_MANIFEST_SUFFIX}"
-    # e.g. "grapl-service.artifacts.json"
-    local -r artifacts_file="$(artifacts_file_for "${packer_image_name}")"
-
-    # Download Packer manifest
-    echo -e "--- :buildkite: Download Packer manifest"
-    buildkite-agent artifact download "${manifest_file}" .
-
-    # TODO wimax July 2021: The below is not correct for Grapl's AMIs.
-    # We have many regions.
-
-    # Extract AMI ID
-    echo -e "--- :gear: Extracting AMI ID"
-    # The raw artifact ID is like: "us-east-1:ami-0123456789abcdef0";
-
-    # Creates a dict that looks like
-    # { "imagename.us-east-1": "ami-111", "imagename.us-east-2": "ami-222", ...}
-    local -r ami_ids_dict=$(jq --raw-output --arg IMAGE_NAME "${packer_image_name}" --from-file "${jq_filter_path}" "${manifest_file}")
-    echo "${ami_ids_dict}"
-
-    # Creating artifacts file
-    echo -c "--- :gear: Creating ${artifacts_file} file"
-    echo "${ami_ids_dict}" > "${artifacts_file}"
-}
-
-for image_name in "${PACKER_IMAGE_NAMES[@]}"; do
-    generate_artifacts_file "${image_name}"
-done
diff --git a/.buildkite/shared/scripts/diff.sh b/.buildkite/shared/scripts/diff.sh
index 9a373ddc11..f06ace7b43 100755
--- a/.buildkite/shared/scripts/diff.sh
+++ b/.buildkite/shared/scripts/diff.sh
@@ -25,12 +25,6 @@ case "${pipeline}" in
         # If the last run of the merge pipeline failed, we still want
         # to perform the expensive operations we're using this script
         # to be selective about.
-        #
-        # For instance, the failing run may have been building Packer
-        # AMI images, but failed due to an unrelated issue in the
-        # build scripts, or something totally unrelated to the Packer
-        # source files. The fix would then not touch those source
-        # files, and we would never rebuild the image.
         git diff --name-only "$(tag_for_pipeline "${pipeline}")"
         ;;
     verify)
diff --git a/.gitattributes b/.gitattributes
index 5ad2cc7e22..8fc40062f6 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,2 @@
 # Hide generated files during diff
-src/js/grapl-cdk/edge_ux_pre_replace/**/* linguist-generated=true
+3rdparty/python/*.lockfile linguist-generated=true
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 4efca3efca..ebc95b99b5 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -28,7 +28,6 @@ BUILD @christophermaier
 /LOCAL_UI_WORKFLOW.md @christophermaier
 /Makefile @christophermaier
 /nomad @d0nut-grapl
-/packer/ @wimax-grapl @christophermaier
 /pants @christophermaier
 /pants.ci.toml @christophermaier
 /pants-plugins/ @christophermaier
@@ -49,14 +48,13 @@ BUILD @christophermaier
 /src/python/grapl-common/ @wimax-grapl
 /src/python/graplctl/ @jgrillo-grapl
 /src/python/grapl-dgraph-ttl/ @grapl-security/wg-data-infra
-/src/python/graplinc-grapl-api/ @grapl-security/wg-data-infra
 /src/python/grapl-model-plugin-deployer/ @colin-grapl
 /src/python/grapl-notebook/ @wimax-grapl
 /src/python/grapl-template-generator/ @wimax-grapl @colin-grapl
 /src/python/grapl-tests-common/ @wimax-grapl
 /src/python/mypy.ini @christophermaier @wimax-grapl
 /src/python/provisioner/ @jgrillo-grapl
-/src/python/python_test_deps/ @christophermaier
+/src/python/python-proto/ @grapl-security/wg-data-infra
 /src/rust/analyzer-dispatcher/ @grapl-security/wg-data-infra
 /src/rust/bin/ @christophermaier
 /src/rust/.cargo @christophermaier
@@ -65,7 +63,6 @@ BUILD @christophermaier
 /src/rust/derive-dynamic-node/ @colin-grapl
 /src/rust/endpoint-plugin/ @inickles-grapl
 /src/rust/generators/ @inickles-grapl
-/src/rust/graph-descriptions/ @grapl-security/wg-data-infra
 /src/rust/graph-merger/ @grapl-security/wg-data-infra
 /src/rust/grapl-config/ @inickles-grapl @wimax-grapl
 /src/rust/grapl-graphql-codegen/ @colin-grapl
@@ -77,6 +74,7 @@ BUILD @christophermaier
 /src/rust/kafka-metrics-exporter/ @jgrillo-grapl
 /src/rust/metric-forwarder/ @wimax-grapl
 /src/rust/node-identifier/ @grapl-security/wg-data-infra
+/src/rust/rust-proto/ @grapl-security/wg-data-infra
 /src/rust/rust-toolchain @inickles-grapl
 /src/rust/sqs-executor/ @inickles-grapl @grapl-security/wg-data-infra
 /src/rust/sysmon/ @inickles-grapl
diff --git a/3rdparty/python/black.lockfile b/3rdparty/python/black.lockfile
new file mode 100644
index 0000000000..674b0ba614
--- /dev/null
+++ b/3rdparty/python/black.lockfile
@@ -0,0 +1,119 @@
+# This lockfile was autogenerated by Pants. To regenerate, run:
+#
+#    ./pants generate-lockfiles --resolve=black
+#
+# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---
+# {
+#   "requirements_invalidation_digest": "e4d0cd266ee4028f3c454414b812360e58f122fbdc820022ef4ab19f18541e8f",
+#   "valid_for_interpreter_constraints": [
+#     "CPython>=3.6.2"
+#   ]
+# }
+# --- END PANTS LOCKFILE METADATA ---
+
+appdirs==1.4.4; python_version >= "3.6" \
+    --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 \
+    --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41
+black==20.8b1; python_version >= "3.6" \
+    --hash=sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea
+click==8.0.1; python_version >= "3.6" \
+    --hash=sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6 \
+    --hash=sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a
+colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.6" and python_full_version >= "3.5.0" \
+    --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 \
+    --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b
+dataclasses==0.8; python_version >= "3.6" and python_version < "3.7" \
+    --hash=sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf \
+    --hash=sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97
+importlib-metadata==4.8.1; python_version < "3.8" and python_version >= "3.6" \
+    --hash=sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15 \
+    --hash=sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1
+mypy-extensions==0.4.3; python_version >= "3.6" \
+    --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \
+    --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8
+pathspec==0.9.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
+    --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \
+    --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1
+regex==2021.8.28; python_version >= "3.6" \
+    --hash=sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2 \
+    --hash=sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a \
+    --hash=sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0 \
+    --hash=sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb \
+    --hash=sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a \
+    --hash=sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308 \
+    --hash=sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed \
+    --hash=sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8 \
+    --hash=sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c \
+    --hash=sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c \
+    --hash=sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13 \
+    --hash=sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0 \
+    --hash=sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1 \
+    --hash=sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f \
+    --hash=sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354 \
+    --hash=sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645 \
+    --hash=sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a \
+    --hash=sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e \
+    --hash=sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892 \
+    --hash=sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791 \
+    --hash=sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759 \
+    --hash=sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906 \
+    --hash=sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a \
+    --hash=sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc \
+    --hash=sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd \
+    --hash=sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797 \
+    --hash=sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f \
+    --hash=sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256 \
+    --hash=sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b \
+    --hash=sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e \
+    --hash=sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d \
+    --hash=sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2 \
+    --hash=sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468 \
+    --hash=sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb \
+    --hash=sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d \
+    --hash=sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983 \
+    --hash=sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8 \
+    --hash=sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed \
+    --hash=sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374 \
+    --hash=sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73 \
+    --hash=sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1
+toml==0.10.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" \
+    --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+    --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+typed-ast==1.4.3; python_version >= "3.6" \
+    --hash=sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6 \
+    --hash=sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075 \
+    --hash=sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528 \
+    --hash=sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428 \
+    --hash=sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3 \
+    --hash=sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f \
+    --hash=sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341 \
+    --hash=sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace \
+    --hash=sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f \
+    --hash=sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363 \
+    --hash=sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7 \
+    --hash=sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266 \
+    --hash=sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e \
+    --hash=sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04 \
+    --hash=sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899 \
+    --hash=sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c \
+    --hash=sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805 \
+    --hash=sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a \
+    --hash=sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff \
+    --hash=sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41 \
+    --hash=sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39 \
+    --hash=sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927 \
+    --hash=sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40 \
+    --hash=sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3 \
+    --hash=sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4 \
+    --hash=sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0 \
+    --hash=sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3 \
+    --hash=sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808 \
+    --hash=sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c \
+    --hash=sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65
+typing-extensions==3.10.0.2; python_version < "3.8" and python_version >= "3.6" \
+    --hash=sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7 \
+    --hash=sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34 \
+    --hash=sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e
+zipp==3.5.0; python_version < "3.8" and python_version >= "3.6" \
+    --hash=sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3 \
+    --hash=sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4
diff --git a/3rdparty/python/constraints.txt b/3rdparty/python/constraints.txt
index 5473f70f69..74434ee72e 100644
--- a/3rdparty/python/constraints.txt
+++ b/3rdparty/python/constraints.txt
@@ -1,28 +1,28 @@
-# Generated by build-support/manage_virtualenv.sh on Tue 07 Sep 2021 03:36:31 PM PDT
+# Generated by ./build-support/manage_virtualenv.sh on Fri 24 Sep 2021 05:36:56 PM PDT
 # DO NOT EDIT BY HAND
 ansicolors==1.1.8
-argon2-cffi==20.1.0
+argon2-cffi==21.1.0
 Arpeggio==1.10.2
 arrow==1.1.1
 attrs==20.3.0
 binaryornot==0.4.4
 blessed==1.17.6
-boto3==1.18.37
-botocore==1.21.37
-bugout==0.1.17
+boto3==1.18.48
+botocore==1.21.48
 certifi==2021.5.30
 cffi==1.14.6
 chalice==1.22.4
 chardet==4.0.0
-charset-normalizer==2.0.4
+charset-normalizer==2.0.6
 click==7.1.2
 cookiecutter==1.7.3
 dill==0.3.4
 fasteners==0.16
 grpcio==1.40.0
-humbug==0.1.9
-hypothesis==6.18.0
+humbug==0.2.6
+hypothesis==6.22.0
 idna==3.2
+ijson==3.1.4
 importlib-metadata==4.8.1
 iniconfig==1.1.1
 inquirer==2.7.0
@@ -30,28 +30,28 @@ Jinja2==3.0.1
 jinja2-time==0.2.0
 jmespath==0.10.0
 MarkupSafe==2.0.1
-mypy-boto3-cloudwatch==1.18.37
-mypy-boto3-dynamodb==1.18.37
-mypy-boto3-ec2==1.18.37
-mypy-boto3-lambda==1.18.37
-mypy-boto3-route53==1.18.37
-mypy-boto3-s3==1.18.37
-mypy-boto3-sagemaker==1.18.37
-mypy-boto3-secretsmanager==1.18.37
-mypy-boto3-sns==1.18.36
-mypy-boto3-sqs==1.18.37
-mypy-boto3-ssm==1.18.37
+mypy-boto3-cloudwatch==1.18.48
+mypy-boto3-dynamodb==1.18.48
+mypy-boto3-ec2==1.18.48
+mypy-boto3-lambda==1.18.48
+mypy-boto3-route53==1.18.48
+mypy-boto3-s3==1.18.48
+mypy-boto3-sagemaker==1.18.48
+mypy-boto3-secretsmanager==1.18.48
+mypy-boto3-sns==1.18.48
+mypy-boto3-sqs==1.18.48
+mypy-boto3-ssm==1.18.48
 mypy-extensions==0.4.3
-packaging==20.9
-pantsbuild.pants==2.6.0rc1
+packaging==21.0
+pantsbuild.pants==2.7.0rc2
 parver==0.3.1
-pex==2.1.42
+pex==2.1.44
 pip==21.1.3
 pluggy==1.0.0
 poyo==0.5.0
-protobuf==3.17.3
+protobuf==3.18.0
 psutil==5.8.0
-pulumi==3.11.0
+pulumi==3.13.0
 pulumi-aws==4.6.0
 pulumi-docker==3.1.0
 pulumi-kafka==3.0.1
@@ -76,19 +76,18 @@ requests==2.26.0
 s3transfer==0.5.0
 semver==2.13.0
 setproctitle==1.2.2
-setuptools==56.2.0
+setuptools==57.5.0
 six==1.16.0
 sortedcontainers==2.4.0
 text-unidecode==1.3
 toml==0.10.2
 typer==0.3.2
 types-PyYAML==5.4.3
-types-requests==2.25.0
 types-setuptools==57.0.0
 types-toml==0.1.3
 typing-extensions==3.7.4.3
-urllib3==1.26.6
+urllib3==1.26.7
 wcwidth==0.2.5
-wheel==0.36.2
+wheel==0.37.0
 zipp==3.5.0
 zstd==1.4.9.1
diff --git a/3rdparty/python/isort.lockfile b/3rdparty/python/isort.lockfile
new file mode 100644
index 0000000000..1e9f65b476
--- /dev/null
+++ b/3rdparty/python/isort.lockfile
@@ -0,0 +1,16 @@
+# This lockfile was autogenerated by Pants. To regenerate, run:
+#
+#    ./pants generate-lockfiles --resolve=isort
+#
+# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---
+# {
+#   "requirements_invalidation_digest": "22ff96400e7be4243e240fffd41002809fe3b5c1a0b97d2913a12c4b39ddd18c",
+#   "valid_for_interpreter_constraints": [
+#     "CPython<4,>=3.6"
+#   ]
+# }
+# --- END PANTS LOCKFILE METADATA ---
+
+isort==5.6.4; python_version >= "3.6" and python_version < "4.0" \
+    --hash=sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7 \
+    --hash=sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58
diff --git a/3rdparty/python/mypy.lockfile b/3rdparty/python/mypy.lockfile
new file mode 100644
index 0000000000..826aebf3cc
--- /dev/null
+++ b/3rdparty/python/mypy.lockfile
@@ -0,0 +1,74 @@
+# This lockfile was autogenerated by Pants. To regenerate, run:
+#
+#    ./pants generate-lockfiles --resolve=mypy
+#
+# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---
+# {
+#   "requirements_invalidation_digest": "ba34f75fbfe36c23b294c4bc94de650a07f318dc521e59adaf89c1445604231a",
+#   "valid_for_interpreter_constraints": [
+#     "CPython>=3.6"
+#   ]
+# }
+# --- END PANTS LOCKFILE METADATA ---
+
+mypy-extensions==0.4.3; python_version >= "3.5" \
+    --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \
+    --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8
+mypy==0.812; python_version >= "3.5" \
+    --hash=sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49 \
+    --hash=sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c \
+    --hash=sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521 \
+    --hash=sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb \
+    --hash=sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a \
+    --hash=sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c \
+    --hash=sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6 \
+    --hash=sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064 \
+    --hash=sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56 \
+    --hash=sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8 \
+    --hash=sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7 \
+    --hash=sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564 \
+    --hash=sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506 \
+    --hash=sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5 \
+    --hash=sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66 \
+    --hash=sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e \
+    --hash=sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a \
+    --hash=sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a \
+    --hash=sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97 \
+    --hash=sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df \
+    --hash=sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4 \
+    --hash=sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119
+typed-ast==1.4.3; python_version >= "3.5" \
+    --hash=sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6 \
+    --hash=sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075 \
+    --hash=sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528 \
+    --hash=sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428 \
+    --hash=sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3 \
+    --hash=sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f \
+    --hash=sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341 \
+    --hash=sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace \
+    --hash=sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f \
+    --hash=sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363 \
+    --hash=sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7 \
+    --hash=sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266 \
+    --hash=sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e \
+    --hash=sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04 \
+    --hash=sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899 \
+    --hash=sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c \
+    --hash=sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805 \
+    --hash=sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a \
+    --hash=sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff \
+    --hash=sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41 \
+    --hash=sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39 \
+    --hash=sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927 \
+    --hash=sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40 \
+    --hash=sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3 \
+    --hash=sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4 \
+    --hash=sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0 \
+    --hash=sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3 \
+    --hash=sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808 \
+    --hash=sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c \
+    --hash=sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65
+typing-extensions==3.10.0.2; python_version >= "3.5" \
+    --hash=sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7 \
+    --hash=sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34 \
+    --hash=sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e
diff --git a/3rdparty/python/requirements.txt b/3rdparty/python/requirements.txt
index 5553b4802a..9cc644b89a 100644
--- a/3rdparty/python/requirements.txt
+++ b/3rdparty/python/requirements.txt
@@ -41,6 +41,4 @@ setuptools
 toml==0.10.2
 typer==0.3.2
 typing_extensions
-# pants package with pants 2.6.0rc1 currently requires wheel < 0.37.
-wheel>=0.36,<0.37
 zstd==1.4.9.1
diff --git a/Dockerfile.pulumi b/Dockerfile.pulumi
index 85130c3984..94c432e537 100644
--- a/Dockerfile.pulumi
+++ b/Dockerfile.pulumi
@@ -16,7 +16,7 @@ USER grapl
 ENV USER=grapl
 WORKDIR /home/grapl
 
-RUN curl -O https://get.pulumi.com/releases/sdk/pulumi-v3.9.1-linux-x64.tar.gz && \
+RUN curl -O https://get.pulumi.com/releases/sdk/pulumi-v3.12.0-linux-x64.tar.gz && \
     tar -xzvf pulumi-*.tar.gz && \
     mv pulumi pulumi-bin
 ENV PATH=/home/grapl/pulumi-bin:${PATH}
diff --git a/Makefile b/Makefile
index cd67e07ac0..00d489bb35 100644
--- a/Makefile
+++ b/Makefile
@@ -10,6 +10,7 @@ RUST_BUILD ?= debug
 UID = $(shell id -u)
 GID = $(shell id -g)
 PWD = $(shell pwd)
+GRAPL_ROOT = ${PWD}
 COMPOSE_USER=${UID}:${GID}
 DOCKER_BUILDX_BAKE_OPTS ?=
 ifneq ($(GRAPL_RUST_ENV_FILE),)
@@ -24,7 +25,6 @@ export EVERY_COMPOSE_FILE=--file docker-compose.yml \
 	--file ./test/docker-compose.unit-tests-js.yml \
 	--file ./test/docker-compose.integration-tests.build.yml \
 	--file ./test/docker-compose.e2e-tests.build.yml \
-	--file ./test/docker-compose.typecheck-tests.yml
 
 DOCKER_BUILDX_BAKE := docker buildx bake $(DOCKER_BUILDX_BAKE_OPTS)
 
@@ -128,6 +128,11 @@ help: ## Print this help
 
 ##@ Build 🔨
 
+.PHONY: build-service-pexs
+build-service-pexs:
+	./pants package ./src/python/analyzer_executor/src
+	./pants package ./src/python/engagement-creator/engagement_creator:pex
+
 .PHONY: build-test-unit
 build-test-unit:
 	$(DOCKER_BUILDX_BAKE) \
@@ -160,7 +165,7 @@ build-test-e2e: build
 	$(DOCKER_BUILDX_BAKE) --file ./test/docker-compose.e2e-tests.build.yml
 
 .PHONY: build-lambda-zips
-build-lambda-zips: build-lambda-zips-rust build-lambda-zips-js build-lambda-zips-python ## Generate all lambda zip files
+build-lambda-zips: build-lambda-zips-rust build-lambda-zips-js build-lambda-zips-python build-service-pexs ## Generate all lambda zip files
 
 .PHONY: build-lambda-zips-rust
 build-lambda-zips-rust: ## Build Rust lambda zips
@@ -199,7 +204,7 @@ build-python-wheels:  ## Build all Python wheels
 	./pants filter --target-type=python_distribution :: | xargs ./pants package
 
 .PHONY: build-docker-images-local
-build-docker-images-local: 
+build-docker-images-local:
 	$(WITH_LOCAL_GRAPL_ENV) \
 	$(MAKE) build-docker-images
 
@@ -278,25 +283,16 @@ test-unit-js: build-test-unit-js ## Build and run unit tests - JavaScript only
 	test/docker-compose-with-error.sh
 	$(MAKE) -C src/js/engagement_view test
 
-.PHONY: test-typecheck-docker
-test-typecheck-docker: export COMPOSE_PROJECT_NAME := grapl-typecheck_tests
-test-typecheck-docker: export COMPOSE_FILE := ./test/docker-compose.typecheck-tests.yml
-test-typecheck-docker: build-test-typecheck ## Build and run typecheck tests (non-Pants)
-	test/docker-compose-with-error.sh
-
-.PHONY: test-typecheck-pants
-test-typecheck-pants: ## Typecheck Python code with Pants
-	./pants typecheck ::
-
 .PHONY: test-typecheck
-test-typecheck: test-typecheck-docker test-typecheck-pants ## Typecheck all Python Code
+test-typecheck: ## Typecheck Python Code
+	./pants typecheck ::
 
 .PHONY: test-integration
 test-integration: export COMPOSE_PROJECT_NAME := $(COMPOSE_PROJECT_INTEGRATION_TESTS)
 test-integration: build-test-integration ## Build and run integration tests
 	$(WITH_LOCAL_GRAPL_ENV)
 	export SHOULD_DEPLOY_INTEGRATION_TESTS=True  # This gets read in by `docker-compose.yml`'s pulumi
-	$(MAKE) test-with-env EXEC_TEST_COMMAND="nomad/local/run_parameterized_job.sh integration-tests"
+	$(MAKE) test-with-env EXEC_TEST_COMMAND="nomad/local/run_parameterized_job.sh integration-tests 8"
 
 .PHONY: test-grapl-template-generator
 test-grapl-template-generator:  # Test that the Grapl Template Generator spits out something compilable.
@@ -307,7 +303,7 @@ test-e2e: export COMPOSE_PROJECT_NAME := $(COMPOSE_PROJECT_E2E_TESTS)
 test-e2e: build-test-e2e ## Build and run e2e tests
 	$(WITH_LOCAL_GRAPL_ENV)
 	export SHOULD_DEPLOY_E2E_TESTS=True  # This gets read in by `docker-compose.yml`'s pulumi
-	$(MAKE) test-with-env EXEC_TEST_COMMAND="nomad/local/run_parameterized_job.sh e2e-tests"
+	$(MAKE) test-with-env EXEC_TEST_COMMAND="nomad/local/run_parameterized_job.sh e2e-tests 6"
 
 # This target is not intended to be used directly from the command line, it's
 # intended for tests in docker-compose files that need the Grapl environment.
@@ -340,6 +336,7 @@ test-with-env: # (Do not include help text - not to be used directly)
 	# Bring up the Grapl environment and detach
 	$(MAKE) up-detach
 	# Run tests and check exit codes from each test container
+	echo "--- Executing test with environment"
 	$${EXEC_TEST_COMMAND}
 
 ##@ Lint 🧹
@@ -366,7 +363,7 @@ lint-prettier: build-formatter ## Run ts/js/yaml lint checks
 		run --rm lint-prettier
 
 .PHONY: lint-hcl
-lint-hcl: ## Check to see if Packer templates are formatted properly
+lint-hcl: ## Check to see if HCL files are formatted properly
 	${NONROOT_DOCKER_COMPOSE_CHECK} hcl-lint
 
 .PHONY: lint-proto
@@ -431,12 +428,13 @@ up-detach: build-local ## Bring up local Grapl and detach to return control to t
 
 	# TODO: This could potentially be replaced with a docker-compose run, but
 	#  it doesn't have all these useful flags
+	echo "--- Starting Pulumi"
 	docker-compose \
 		--file docker-compose.yml \
 		up --force-recreate --always-recreate-deps --renew-anon-volumes \
 		--exit-code-from pulumi \
 		pulumi
-	echo "--- up-detach complete"
+	echo "Pulumi complete"
 
 .PHONY: down
 down: ## docker-compose down - both stops and removes the containers
diff --git a/buf.yaml b/buf.yaml
index 553fbf0477..5e626331f3 100644
--- a/buf.yaml
+++ b/buf.yaml
@@ -7,6 +7,8 @@ lint:
   use:
     - DEFAULT
     - COMMENTS
+    - UNARY_RPC
+    - OTHER
   ignore:
     # We have vendored some of the Google protobuf definitions; we
     # don't need to lint them, and will be able to remove them soon.
diff --git a/docker-compose.build.yml b/docker-compose.build.yml
index 6d7c83c582..4a25a5ab66 100644
--- a/docker-compose.build.yml
+++ b/docker-compose.build.yml
@@ -84,24 +84,14 @@ services:
     build:
       context: .
       dockerfile: ./src/python/Dockerfile
-      target: analyzer-executor-deploy
+      target: analyzer-executor
 
   engagement-creator:
     image: grapl/engagement-creator:${TAG:-latest}
     build:
       context: .
       dockerfile: ./src/python/Dockerfile
-      target: engagement-creator-deploy
-
-  # This container build exists only for Local Grapl; if we are able
-  # to deploy model-plugin-deployer as an actual Lambda there, we can
-  # get rid of this.
-  model-plugin-deployer:
-    image: grapl/model-plugin-deployer:${TAG:-latest}
-    build:
-      context: .
-      dockerfile: ./src/python/Dockerfile
-      target: model-plugin-deployer-deploy
+      target: engagement-creator
 
   ########################################################################
   # Web Services
diff --git a/docker-compose.yml b/docker-compose.yml
index fb36213ae6..1ef44c51f6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -85,74 +85,11 @@ services:
   ########################################################################
   # Python Services
   ########################################################################
-  model-plugin-deployer:
-    image: grapl/model-plugin-deployer:${TAG:-latest}
-    entrypoint: ["/bin/bash", "-o", "errexit", "-o", "nounset", "-c"]
-    command:
-      - |
-        export GRAPL_MODEL_PLUGINS_BUCKET=$$(cat /mnt/pulumi-outputs/model-plugins-bucket)
-        export GRAPL_SCHEMA_TABLE=$$(cat /mnt/pulumi-outputs/schema-table)
-        export GRAPL_SCHEMA_PROPERTIES_TABLE=$$(cat /mnt/pulumi-outputs/schema-properties-table)
-
-        . venv/bin/activate
-        cd /home/grapl/app
-        chalice local \
-          --no-autoreload \
-          --host=0.0.0.0 \
-          --port=${GRAPL_MODEL_PLUGIN_DEPLOYER_PORT}
-
-    environment:
-      <<: *aws-env
-      <<: *dgraph-env
-      IS_LOCAL: "True"
-      <<: *log-level
-    tty: true
-    depends_on:
-      pulumi:
-        # We must wait until Pulumi has created the buckets to know what their names are
-        condition: service_completed_successfully
-    volumes:
-      - *read-only-pulumi-mnt
-    ports:
-      - 127.0.0.1:${GRAPL_MODEL_PLUGIN_DEPLOYER_PORT}:${GRAPL_MODEL_PLUGIN_DEPLOYER_PORT}
-    networks:
-      default:
-        aliases:
-          - ${GRAPL_MODEL_PLUGIN_DEPLOYER_HOST}
 
   ########################################################################
   # Web Services
   ########################################################################
 
-  graphql-endpoint:
-    image: grapl/graphql-endpoint:${TAG:-latest}
-    entrypoint: ["/bin/bash", "-o", "errexit", "-o", "nounset", "-c"]
-    command:
-      - |
-        export GRAPL_SCHEMA_PROPERTIES_TABLE=$$(cat /mnt/pulumi-outputs/schema-properties-table)
-        export GRAPL_SCHEMA_TABLE=$$(cat /mnt/pulumi-outputs/schema-table)
-        ./start_potentially_with_debugger.sh
-    environment:
-      <<: *aws-env
-      <<: *dgraph-env
-      IS_LOCAL: "True"
-      JWT_SECRET_ID: "JWT_SECRET_ID"
-      PORT: ${GRAPL_GRAPHQL_PORT}
-      DEBUG_SERVICES: "${DEBUG_SERVICES:-}"
-      VSC_DEBUGGER_PORT_FOR_GRAPHQL_ENDPOINT: "${VSC_DEBUGGER_PORT_FOR_GRAPHQL_ENDPOINT}"
-    depends_on:
-      pulumi:
-        condition: service_completed_successfully
-    ports:
-      - 127.0.0.1:${GRAPL_GRAPHQL_PORT}:${GRAPL_GRAPHQL_PORT}
-      - 127.0.0.1:${VSC_DEBUGGER_PORT_FOR_GRAPHQL_ENDPOINT}:${VSC_DEBUGGER_PORT_FOR_GRAPHQL_ENDPOINT}
-    networks:
-      default:
-        aliases:
-          - ${GRAPL_GRAPHQL_HOST}
-    volumes:
-      - *read-only-pulumi-mnt
-
   grapl-notebook:
     image: grapl/notebook:${TAG:-latest}
     user: grapl
@@ -229,6 +166,8 @@ services:
       SHOULD_DEPLOY_INTEGRATION_TESTS: "${SHOULD_DEPLOY_INTEGRATION_TESTS:-False}"
       # This gets exported to True by `make test-e2e`
       SHOULD_DEPLOY_E2E_TESTS: "${SHOULD_DEPLOY_E2E_TESTS:-False}"
+      DOCKER_USER: "${UID}:${GID}"
+      GRAPL_ROOT: "${GRAPL_ROOT}"
       # Other environment variables like MG_ALPHAS are passed in via
       # Pulumi.local-grapl.yaml
     extra_hosts:
diff --git a/docs/plugins/models.md b/docs/plugins/models.md
index 33a1fc5320..f7d902bc61 100644
--- a/docs/plugins/models.md
+++ b/docs/plugins/models.md
@@ -38,7 +38,7 @@ our first model - the Ec2Instance.
 `src/lib.rs`
 
     use derive_dynamic_node::{DynamicNode as DeriveDynamicNode, GraplStaticId};
-    use grapl_graph_descriptions::graph_description::*;
+    use rust_proto::graph_descriptions::*;
 
     #[derive(Clone, DeriveDynamicNode, GraplStaticId)]
     struct Ec2Instance {
diff --git a/docs/queryable.md b/docs/queryable.md
index 358af7a2e2..b915c5e34b 100644
--- a/docs/queryable.md
+++ b/docs/queryable.md
@@ -147,10 +147,9 @@ ProcessQuery()
 
 ##### All Together
 
-This query matches a process with:
-
-- a `process_name` that is _not_ 'foo' but ends with '.exe'
-- _or_ a `process_name` that contains both "bar" _and_ "baz".
+This query matches a process with a process*name that either is \_not* 'foo' but
+ends with '.exe', _or_ it will match a process with a process containing "bar"
+_and_ "baz".
 
 ```python3
 ProcessQuery()
diff --git a/nomad/local/.gitignore b/nomad/local/.gitignore
deleted file mode 100644
index 8a61866eb5..0000000000
--- a/nomad/local/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# This file is dynamically written by `start_detach.sh` and 
-# should never be committed.
-mount-grapl-root-as-volume.nomad
\ No newline at end of file
diff --git a/nomad/local/nomad_run_local_infra.sh b/nomad/local/nomad_run_local_infra.sh
index 1b52e835e1..a52b4cd5fc 100755
--- a/nomad/local/nomad_run_local_infra.sh
+++ b/nomad/local/nomad_run_local_infra.sh
@@ -3,7 +3,7 @@ set -euo pipefail
 
 GRAPL_ROOT="$(git rev-parse --show-toplevel)"
 
-echo "Deploying local infrastructure."
+echo "--- Deploying Nomad local infrastructure."
 
 # Wait a short period of time before attempting to deploy infrastructure
 # shellcheck disable=SC2016
@@ -22,4 +22,4 @@ nomad job run \
     -var "FAKE_AWS_SECRET_ACCESS_KEY=${FAKE_AWS_SECRET_ACCESS_KEY}" \
     "${GRAPL_ROOT}"/nomad/local/grapl-local-infra.nomad
 
-echo -e "\n--- Nomad local-infra deployed!"
+echo "--- Nomad local-infra deployed!"
diff --git a/nomad/local/run_parameterized_job.sh b/nomad/local/run_parameterized_job.sh
index 241defbebc..0abc114296 100755
--- a/nomad/local/run_parameterized_job.sh
+++ b/nomad/local/run_parameterized_job.sh
@@ -10,10 +10,11 @@ source "${THIS_DIR}/nomad_cli_tools.sh"
 # Pulumi __main__.py's `NomadJob(...)` calls.
 
 readonly job_to_dispatch="${1}"
+readonly mins_to_wait="${2}"
 
 # Now we have to actually dispatch a job; Pulumi simply uploaded
 # the jobspec, since it's a Parameterized Batch Job.
-echo "--- Dispatching ${job_to_dispatch}"
+echo -e "\n+++ Dispatching Nomad job: ${job_to_dispatch}"
 
 job_id=$(nomad_dispatch "${job_to_dispatch}")
 echo "You can view job progress at $(url_to_nomad_job_in_ui "${job_id}")"
@@ -21,7 +22,7 @@ echo "You can view job progress at $(url_to_nomad_job_in_ui "${job_id}")"
 dispatch_timed_out=0
 await_nomad_dispatch_finish \
     "${job_id}" \
-    $((6 * 60)) \
+    $((mins_to_wait * 60)) \
     "${job_to_dispatch}" ||
     dispatch_timed_out=1
 
diff --git a/nomad/local/start_detach.sh b/nomad/local/start_detach.sh
index 9fb27656ad..427dacd3a8 100755
--- a/nomad/local/start_detach.sh
+++ b/nomad/local/start_detach.sh
@@ -8,9 +8,6 @@ THIS_DIR=$(dirname "${BASH_SOURCE[0]}")
 readonly THIS_DIR
 cd "${THIS_DIR}"
 
-VOLUME_CONFIG_FILE="mount-grapl-root-as-volume.nomad"
-readonly VOLUME_CONFIG_FILE
-
 ensure_valid_env() {
     # Ensure script is being run with `local-grapl.env` variables
     # via `make start-nomad-ci`
@@ -31,32 +28,14 @@ ensure_valid_env() {
     fi
 }
 
-write_mount_grapl_root_volume_config() {
-    # Every time we do a `nomad-agent &`, write a new file to
-    # "per-invoke-nomad-conf.nomad"
-    # that defines a Nomad host volume pointing at ${PWD} (aka, grapl root).
-    # This is required for `python-integration-tests` to run Pants in an `exec` task.
-    cat << EOF > "${VOLUME_CONFIG_FILE}"
-# Written dynamically by 'write_mount_grapl_root_volume_config'
-client {
-  host_volume "grapl-root-volume" {
-    path = "${PWD}"
-    read_only = false
-  }
-}
-EOF
-}
-
 start_nomad_detach() {
     ensure_valid_env
-    write_mount_grapl_root_volume_config
 
     echo "Starting nomad and consul locally. Logs @ ${NOMAD_LOGS_DEST} and ${CONSUL_LOGS_DEST}."
     # These will run forever until `make stop-nomad-ci` is invoked."
     # shellcheck disable=SC2024
     sudo nomad agent \
         -config="nomad-agent-conf.nomad" \
-        -config="${VOLUME_CONFIG_FILE}" \
         -dev-connect > "${NOMAD_LOGS_DEST}" &
     consul agent -dev > "${CONSUL_LOGS_DEST}" &
 
diff --git a/packer/README.md b/packer/README.md
deleted file mode 100644
index 6eff1593ea..0000000000
--- a/packer/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-Both of these AMIs are heavily based off of
-https://github.com/hashicorp/terraform-aws-nomad/tree/master/examples/nomad-consul-ami
-(but converted to HCL)
-
-The difference between these two AMIs are:
-
-- the nomad/consul config files that are provisioned by them.
-- the AMI name prefix var
-
-Really, that's it! They could totally be merged, but I'm leaving us open to a
-potential future where they diverge more.
diff --git a/packer/hax_change_this_to_trigger_ami_build.txt b/packer/hax_change_this_to_trigger_ami_build.txt
deleted file mode 100644
index 19b3f3734e..0000000000
--- a/packer/hax_change_this_to_trigger_ami_build.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-# Sometimes we need to manually retrigger an AMI build.
-# So, as a hack, just change this file to trigger that build.
-# (It is read in `pipeline.merge.yml`'s diff)
-
-# add to this int i guess? leave a funny comment?
-0
\ No newline at end of file
diff --git a/packer/nomad-server-client/configs/consul-config/grapl-client.json b/packer/nomad-server-client/configs/consul-config/grapl-client.json
deleted file mode 100644
index e608378ab4..0000000000
--- a/packer/nomad-server-client/configs/consul-config/grapl-client.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "server": false,
-  "retry_join_ec2": {
-    "tag_key": "ConsulAgent",
-    "tag_value": "Server"
-  },
-  "log_level": "INFO"
-} 
\ No newline at end of file
diff --git a/packer/nomad-server-client/configs/consul-config/grapl-server.json b/packer/nomad-server-client/configs/consul-config/grapl-server.json
deleted file mode 100644
index 5569c20ffb..0000000000
--- a/packer/nomad-server-client/configs/consul-config/grapl-server.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "server": true,
-  "retry_join_ec2": {
-    "tag_key": "ConsulAgent",
-    "tag_value": "Server"
-  },
-  "log_level": "INFO"
-}
\ No newline at end of file
diff --git a/packer/nomad-server-client/configs/nomad-config/grapl-client.hcl b/packer/nomad-server-client/configs/nomad-config/grapl-client.hcl
deleted file mode 100644
index c8362ccf8b..0000000000
--- a/packer/nomad-server-client/configs/nomad-config/grapl-client.hcl
+++ /dev/null
@@ -1,12 +0,0 @@
-server {
-  enabled = false
-}
-
-client {
-  enabled = true
-}
-
-consul {
-  auto_advertise   = true
-  client_auto_join = true
-}
diff --git a/packer/nomad-server-client/configs/nomad-config/grapl-server.hcl b/packer/nomad-server-client/configs/nomad-config/grapl-server.hcl
deleted file mode 100644
index 7b2fffcf81..0000000000
--- a/packer/nomad-server-client/configs/nomad-config/grapl-server.hcl
+++ /dev/null
@@ -1,12 +0,0 @@
-server {
-  enabled = true
-}
-
-client {
-  enabled = false
-}
-
-consul {
-  auto_advertise   = true
-  client_auto_join = true
-}
diff --git a/packer/nomad-server-client/configs/osquery-config/osquery.conf b/packer/nomad-server-client/configs/osquery-config/osquery.conf
deleted file mode 100644
index cc5aeec1b1..0000000000
--- a/packer/nomad-server-client/configs/osquery-config/osquery.conf
+++ /dev/null
@@ -1,71 +0,0 @@
-// This is taken from the bundled osquery.example.conf
-// (with some useless pieces removed)
-
-{
-  // Configure the daemon below:
-  "options": {
-    // Select the osquery config plugin.
-    "config_plugin": "filesystem",
-
-    // Select the osquery logging plugin.
-    "logger_plugin": "filesystem",
-
-    // The log directory stores info, warning, and errors.
-    // If the daemon uses the 'filesystem' logging retriever then the log_dir
-    // will also contain the query results.
-    //"logger_path": "/var/log/osquery",
-
-    // Set 'disable_logging' to true to prevent writing any info, warning, error
-    // logs. If a logging plugin is selected it will still write query results.
-    //"disable_logging": "false",
-
-    // Splay the scheduled interval for queries.
-    // This is very helpful to prevent system performance impact when scheduling
-    // large numbers of queries that run a smaller or similar intervals.
-    //"schedule_splay_percent": "10",
-
-    // A filesystem path for disk-based backing storage used for events and
-    // query results differentials. See also 'use_in_memory_database'.
-    //"database_path": "/var/osquery/osquery.db",
-
-    // Comma-delimited list of table names to be disabled.
-    // This allows osquery to be launched without certain tables.
-    //"disable_tables": "foo_bar,time",
-
-    // Comma-delimited list of table names to be enabled.
-    // This allows osquery to be launched with certain tables only.
-    //"enable_tables": "foo_bar,time",
-
-    "utc": "true"
-  },
-
-  // Decorators are normal queries that append data to every query.
-  "decorators": {
-    "load": [
-      "SELECT uuid AS host_uuid FROM system_info;",
-      "SELECT user AS username FROM logged_in_users ORDER BY time DESC LIMIT 1;"
-    ]
-  },
-
-  // Add default osquery packs or install your own.
-  //
-  // There are several 'default' packs installed with 'make install' or via
-  // packages and/or Homebrew.
-  //
-  // Linux:        /usr/share/osquery/packs
-  // OS X:         /var/osquery/packs
-  // Homebrew:     /usr/local/share/osquery/packs
-  // make install: {PREFIX}/share/osquery/packs
-  //
-  "packs": {
-    // "osquery-monitoring": "/usr/share/osquery/packs/osquery-monitoring.conf",
-    // "incident-response": "/usr/share/osquery/packs/incident-response.conf",
-    // "it-compliance": "/usr/share/osquery/packs/it-compliance.conf",
-    // "osx-attacks": "/usr/share/osquery/packs/osx-attacks.conf",
-    // "vuln-management": "/usr/share/osquery/packs/vuln-management.conf",
-    // "hardware-monitoring": "/usr/share/osquery/packs/hardware-monitoring.conf",
-    // "ossec-rootkit": "/usr/share/osquery/packs/ossec-rootkit.conf",
-    // "windows-hardening": "C:\\Program Files\\osquery\\packs\\windows-hardening.conf",
-    // "windows-attacks": "C:\\Program Files\\osquery\\packs\\windows-attacks.conf"
-  }
-}
diff --git a/packer/nomad-server-client/configs/vector-config/vector.toml b/packer/nomad-server-client/configs/vector-config/vector.toml
deleted file mode 100644
index 32a83fd2f6..0000000000
--- a/packer/nomad-server-client/configs/vector-config/vector.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-# We are trying to follow the 'log shipper pattern':
-# https://learn.hashicorp.com/tutorials/nomad/jobs-accessing-logs
-# Basically, after each Nomad task, you can run a binary afterwards that's just
-# "hey, ship these logs to Kafka!"
-# Disclaimer: I make zero claim that this config works yet - wimax Jul 2021
-[sources.nomad_logs]
-type = "file"
-# TBD: Does this need to be fully qualified path?
-include = ["alloc/logs"]
-
-[sinks.kafka_out]
-inputs = ["in"]
-type = "kafka"
-# TBD: Different topic per service?
-topic = "nomad-logs"
diff --git a/packer/nomad-server-client/image.pkr.hcl b/packer/nomad-server-client/image.pkr.hcl
deleted file mode 100644
index 05c91da394..0000000000
--- a/packer/nomad-server-client/image.pkr.hcl
+++ /dev/null
@@ -1,248 +0,0 @@
-variable "build_ami" {
-  description = "Whether or not to actually build an AMI. Set to `false` for doing testing"
-  type        = bool
-  default     = true
-}
-
-variable "image_name" {
-  description = "The name of this AMI image. Also is included in the packer-manifest name."
-  type        = string
-
-  validation {
-    condition     = contains(["grapl-nomad-consul-server", "grapl-nomad-consul-client"], var.image_name)
-    error_message = "That is not one of 2 accepted image names (grapl-nomad-consul-server, grapl-nomad-consul-client)."
-  }
-}
-
-variable "terraform_aws_consul_tag" {
-  description = "Version tag of terraform-aws-consul to checkout"
-  type        = string
-  default     = "v0.10.1"
-}
-
-variable "git_sha" {
-  description = "The git SHA of the commit the AMI is being generated from. If present, will be used to tag the AMI."
-  type        = string
-  default     = env("GIT_SHA")
-}
-
-variable "git_branch" {
-  description = "The git branch the AMI is being generated from. If present, will be used to tag the AMI."
-  type        = string
-  default     = env("GIT_BRANCH")
-}
-
-variable "buildkite_build_number" {
-  description = "The build number of the Buildkite pipeline this AMI was built in. If present, will be used to tag the AMI."
-  type        = string
-  default     = env("BUILDKITE_BUILD_NUMBER")
-}
-
-variable "buildkite_pipeline_name" {
-  description = "The Buildkite pipeline this AMI was built in. If present, will be used to tag the instance used to build the AMI."
-  type        = string
-  default     = env("BUILDKITE_PIPELINE_NAME")
-}
-
-variable "buildkite_step_key" {
-  description = "The key of the step in the Buildkite pipeline this AMI was built in. If present, will be used to tag the instance used to build the AMI."
-  type        = string
-  default     = env("BUILDKITE_STEP_KEY")
-}
-
-variable "aws_build_region" {
-  description = "Region to build thge AMI in"
-  type        = string
-  default     = "us-east-1"
-}
-
-####################
-# Software Versions
-####################
-
-variable "terraform_aws_nomad_tag" {
-  description = "Version tag of terraform-aws-nomad to checkout"
-  type        = string
-  default     = "v0.9.1"
-}
-
-variable "consul_version" {
-  description = "Version of consul to use"
-  type        = string
-  default     = "1.9.6"
-}
-
-variable "nomad_version" {
-  description = "Version of Nomad to use"
-  type        = string
-  default     = "1.1.1"
-}
-
-variable "vector_version" {
-  description = "Version of Vector to use"
-  type        = string
-  default     = "0.15.0"
-}
-
-variable "osquery_version" {
-  description = "Version of OSQuery to use"
-  type        = string
-  default     = "4.9.0"
-}
-
-locals {
-  # These are various metadata tags we can add to the resulting
-  # AMI. If any are unset (like the Buildkite build number, if
-  # building outside of Buildkite), those tags will be filtered out
-  # and not added to the AMI.
-  tags_from_vars = {
-    "git:sha"                = "${var.git_sha}"
-    "git:branch"             = "${var.git_branch}"
-    "buildkite:build_number" = "${var.buildkite_build_number}"
-  }
-
-  # These tags will be added to the instance used to build the AMI. If
-  # they are not present, no tags will be added.
-  run_tags_from_env = {
-    "buildkite:pipeline"     = "${var.buildkite_pipeline_name}"
-    "buildkite:build_number" = "${var.buildkite_build_number}"
-    "buildkite:step"         = "${var.buildkite_step_key}"
-  }
-
-  formatted_timestamp = formatdate("YYYYMMDDhhmmss", timestamp())
-  copy_ami_to_regions = [
-    "us-east-2",
-    "us-west-1",
-    "us-west-2",
-  ]
-
-  is_server = var.image_name == "grapl-nomad-consul-server"
-
-  # The names of the <client or server> files in consul-config & nomad-config
-  # Fun fact: alphabetically, they need to be after "default"
-  config_file_names = local.is_server ? "grapl-server*" : "grapl-client*"
-}
-
-data "amazon-ami" "amazon-linux-2-x86_64" {
-  filters = {
-    architecture        = "x86_64"
-    name                = "*amzn2-ami-hvm-*"
-    root-device-type    = "ebs"
-    virtualization-type = "hvm"
-
-    # Yes, quoting is required. This is an Amazon-defined key and not a Packer concept.
-    # https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeImages.html
-    "block-device-mapping.volume-type" = "gp2"
-  }
-  most_recent = true
-  owners      = ["amazon"]
-  region      = var.aws_build_region
-}
-
-source "amazon-ebs" "amazon-linux-2-amd64-ami" {
-  ami_description = "An Amazon Linux 2 x86_64 AMI that has Nomad and Consul installed."
-  ami_name        = "${var.image_name}-al2-x64-${local.formatted_timestamp}"
-  instance_type   = "t2.micro"
-  region          = var.aws_build_region
-  # Where we copy it after it's built
-  ami_regions  = local.copy_ami_to_regions
-  source_ami   = data.amazon-ami.amazon-linux-2-x86_64.id
-  ssh_username = "ec2-user"
-  # Make the image public.
-  # As such, we should not bake credentials of any kind into the image
-  ami_groups = ["all"]
-
-  tag {
-    key   = "BuiltBy"
-    value = "Packer ${packer.version}"
-  }
-
-  # Filter out any variable-based tags that are unset.
-  dynamic "tag" {
-    for_each = { for key, value in local.tags_from_vars : key => value if value != "" }
-    content {
-      key   = tag.key
-      value = tag.value
-    }
-  }
-
-  dynamic "run_tag" {
-    for_each = { for key, value in local.run_tags_from_env : key => value if value != "" }
-    content {
-      key   = run_tag.key
-      value = run_tag.value
-    }
-  }
-
-  skip_create_ami = "${var.build_ami == true ? false : true}"
-  ssh_timeout     = "10m"
-}
-
-build {
-  sources = ["source.amazon-ebs.amazon-linux-2-amd64-ami"]
-
-  provisioner "shell" {
-    inline = [
-      "sudo yum install --assumeyes git",
-    ]
-  }
-
-  #################### 
-  # Install software
-  #################### 
-
-  provisioner "shell" {
-    environment_vars = [
-      "NOMAD_VERSION=${var.nomad_version}",
-      "CONSUL_VERSION=${var.consul_version}",
-      "TERRAFORM_AWS_CONSUL_TAG=${var.terraform_aws_consul_tag}",
-      "TERRAFORM_AWS_NOMAD_TAG=${var.terraform_aws_nomad_tag}",
-    ]
-    script = "${path.root}/setup_nomad_consul.sh"
-  }
-
-  provisioner "shell" {
-    environment_vars = [
-      "VECTOR_VERSION=${var.vector_version}",
-    ]
-    script = "${path.root}/setup_vector.sh"
-  }
-
-  provisioner "shell" {
-    environment_vars = [
-      "OSQUERY_VERSION=${var.osquery_version}",
-    ]
-    script = "${path.root}/setup_osquery.sh"
-  }
-
-  #################### 
-  # Copy over config files
-  #################### 
-
-  # Copy Nomad/Consul/Vector/etc configs to a temp spot.
-  # We can't copy directly into, say, `/opt` because we need sudo perms.
-  provisioner "file" {
-    source      = "${path.root}/configs"
-    destination = "/tmp/configs"
-  }
-
-  # Copy the above configs into their final spots
-  provisioner "shell" {
-    inline = [
-      # Only copy <the client> or <the server> file names
-      "sudo cp /tmp/configs/nomad-config/${local.config_file_names} /opt/nomad/config",
-      "sudo cp /tmp/configs/consul-config/${local.config_file_names} /opt/consul/config",
-
-      "sudo cp /tmp/configs/vector-config/vector.toml /etc/vector/vector.toml",
-      "sudo cp /tmp/configs/osquery-config/osquery.conf /etc/osquery/osquery.conf",
-    ]
-  }
-
-  #################### 
-  # Post-processors
-  #################### 
-
-  post-processor "manifest" {
-    output = "${var.image_name}.packer-manifest.json"
-  }
-}
diff --git a/packer/nomad-server-client/setup_nomad_consul.sh b/packer/nomad-server-client/setup_nomad_consul.sh
deleted file mode 100755
index d3b43e10b3..0000000000
--- a/packer/nomad-server-client/setup_nomad_consul.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-set -euo pipefail
-
-# As recommended in https://github.com/hashicorp/terraform-aws-nomad/tree/master/examples/nomad-consul-ami readme
-git clone --branch ${TERRAFORM_AWS_NOMAD_TAG} https://github.com/hashicorp/terraform-aws-nomad.git /tmp/terraform-aws-nomad
-/tmp/terraform-aws-nomad/modules/install-nomad/install-nomad --version "${NOMAD_VERSION}"
-
-git clone --branch "${TERRAFORM_AWS_CONSUL_TAG}"  https://github.com/hashicorp/terraform-aws-consul.git /tmp/terraform-aws-consul
-/tmp/terraform-aws-consul/modules/install-consul/install-consul --version "${CONSUL_VERSION}"
diff --git a/packer/nomad-server-client/setup_osquery.sh b/packer/nomad-server-client/setup_osquery.sh
deleted file mode 100644
index 85542164b7..0000000000
--- a/packer/nomad-server-client/setup_osquery.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/sh
-
-# Adapted from "Alternative Install Options -> RPM Linux" at
-# https://osquery.io/downloads/official/4.9.0
-
-set -euo pipefail
-
-echo "--- Add repo"
-curl -L https://pkg.osquery.io/rpm/GPG | sudo tee /etc/pki/rpm-gpg/RPM-GPG-KEY-osquery
-sudo yum-config-manager --add-repo https://pkg.osquery.io/rpm/osquery-s3-rpm.repo
-sudo yum-config-manager --enable osquery-s3-rpm-repo
-
-echo "--- Install osquery"
-sudo yum install --assumeyes osquery
-
-echo "--- Ensure it's all working correctly"
-osqueryi --version
-# Yes, the `i` at the end is not a typo
\ No newline at end of file
diff --git a/packer/nomad-server-client/setup_vector.sh b/packer/nomad-server-client/setup_vector.sh
deleted file mode 100644
index 72d1216379..0000000000
--- a/packer/nomad-server-client/setup_vector.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-# Adapted from:
-# - https://vector.dev/docs/setup/quickstart/
-# - https://vector.dev/docs/setup/installation/package-managers/yum/
-set -euo pipefail
-
-echo "--- Add repo"
-curl -1sLf \
-  'https://repositories.timber.io/public/vector/cfg/setup/bash.rpm.sh' \
-| sudo -E bash
-
-echo "--- Install Vector"
-sudo yum install --assumeyes "vector-${VECTOR_VERSION}"
-
-echo "--- Ensure it's all working correctly"
-vector --version
\ No newline at end of file
diff --git a/pants.toml b/pants.toml
index 9190726487..ebcf2d8b39 100644
--- a/pants.toml
+++ b/pants.toml
@@ -1,5 +1,5 @@
 [GLOBAL]
-pants_version = "2.6.0rc1"
+pants_version = "2.7.0rc2"
 backend_packages = [
   "pants.backend.awslambda.python",
   "pants.backend.codegen.protobuf.python",
@@ -26,7 +26,7 @@ pants_ignore = [
 ]
 
 plugins = [
-  "toolchain.pants.plugin==0.13.1"
+  "toolchain.pants.plugin==0.14.0"
 ]
 
 remote_cache_read = true
@@ -72,11 +72,11 @@ root_patterns = [
   "/src/python/graplctl",
   "/src/python/grapl-dgraph-ttl",
   "/src/python/grapl_e2e_tests",
-  "/src/python/graplinc",
   "/src/python/grapl-model-plugin-deployer",
   "/src/python/grapl-template-generator",
   "/src/python/grapl-tests-common",
   "/src/python/provisioner",
+  "/src/python/python-proto",
   "/src/python/repl",
 ]
 
@@ -96,10 +96,12 @@ runtime_dependencies = ["3rdparty/python:protobuf"]
 mypy_plugin = true
 
 [black]
+lockfile = "3rdparty/python/black.lockfile"
 config = "pyproject.toml"
 version = "black==20.8b1"
 
 [isort]
+lockfile = "3rdparty/python/isort.lockfile"
 config = "pyproject.toml"
 # This option is required to ensure the `skip_globs` configuration is
 # honored for how Pants calls isort. If we ever get to the point where
@@ -109,7 +111,8 @@ args = [ "--filter-files" ]
 version = "isort==5.6.4"
 
 [pytest]
-pytest_plugins = [
+lockfile = "3rdparty/python/pytest.lockfile"
+extra_requirements = [
   "pytest-custom_exit_code",
   "pytest-cov"
 ]
@@ -125,9 +128,16 @@ pytest_plugins = [
 args = [ "--suppress-no-test-exit-code" ]
 
 [mypy]
+lockfile = "3rdparty/python/mypy.lockfile"
 version = "mypy==0.812"
 config = "src/python/mypy.ini"
-args = [ "--show-error-codes" ] #, "--verbose" ]
+# read this section of the mypy docs:
+#
+# https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-namespace-packages
+#
+# for the raison d'être for "--namespace-packages". Tl;dr: it makes mypy work
+# with our protobuf stubs.
+args = [ "--show-error-codes", "--namespace-packages" ] #, "--verbose" ]
 
 [ipython]
 version = "ipython==7.20.0"
@@ -163,6 +173,6 @@ global_report = "true"
 # configuration values.
 version = "v0.7.2"
 known_versions = [
-  "v0.7.2|darwin|969bd7ef668e8167cfbba569fb9f4a0b2fc1c4021f87032b6a0b0e525fb77369|3988092",
-  "v0.7.2|linux|70423609f27b504d6c0c47e340f33652aea975e45f312324f2dbf91c95a3b188|1382204"
+  "v0.7.2|macos_x86_64|969bd7ef668e8167cfbba569fb9f4a0b2fc1c4021f87032b6a0b0e525fb77369|3988092",
+  "v0.7.2|linux_x86_64|70423609f27b504d6c0c47e340f33652aea975e45f312324f2dbf91c95a3b188|1382204"
 ]
diff --git a/pulumi/README.md b/pulumi/README.md
index 3d55e80e50..64aec54e26 100644
--- a/pulumi/README.md
+++ b/pulumi/README.md
@@ -82,7 +82,7 @@ pulumi login --local
 pulumi stack init "${STACK_NAME}"
 pulumi config set aws:region us-east-1
 
-# Copy some required artifacts - namely AMIs - from `origin/rc`
+# Copy some required artifacts version pins from `origin/rc`
 ../bin/copy_artifacts_from_rc.sh "${STACK_NAME}"
 
 # You likely want to remove non-required artifacts from your stackfile now
diff --git a/pulumi/grapl/__main__.py b/pulumi/grapl/__main__.py
index 79e84b5d62..65b5d5eb04 100644
--- a/pulumi/grapl/__main__.py
+++ b/pulumi/grapl/__main__.py
@@ -223,20 +223,25 @@ def _get_integration_test_job_vars(
                     k: inputs[k]
                     for k in {
                         "_aws_endpoint",
-                        "_kafka_endpoint",
+                        "_kafka_endpoint",  # integration-test only
                         "_redis_endpoint",
                         "aws_access_key_id",
                         "aws_access_key_secret",
                         "aws_region",
                         "deployment_name",
+                        # integration-test only
                         "schema_properties_table_name",
                         "test_user_name",
+                        "grapl_root",
+                        "docker_user",
                     }
                 }
 
             integration_test_job_vars = _get_integration_test_job_vars(
                 dict(
                     _kafka_endpoint=kafka_endpoint,
+                    grapl_root=os.environ["GRAPL_ROOT"],
+                    docker_user=os.environ["DOCKER_USER"],
                     **grapl_core_job_vars_inputs,
                     **nomad_inputs,
                 )
diff --git a/pulumi/infra/ami_artifacts.py b/pulumi/infra/ami_artifacts.py
deleted file mode 100644
index 98affe69b0..0000000000
--- a/pulumi/infra/ami_artifacts.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from typing import Dict, Union
-
-import pulumi_aws as aws
-from infra.config import ArtifactException, require_artifact
-from typing_extensions import Literal
-
-PackerImageName = Union[
-    # Compare with image.pkr.hcl's variable "image_name"
-    Literal["grapl-nomad-consul-server"],
-    Literal["grapl-nomad-consul-client"],
-]
-
-
-def get_ami_id(packer_image_name: PackerImageName) -> str:
-    """
-    Grab AMI IDs from your stack file (Pulumi.stackname.yaml).
-    There's a good chance those values don't exist for you yet, in which case,
-    follow the instructions in `require_artifact`.
-    """
-    region_to_ami: Dict[str, str] = require_artifact(f"{packer_image_name}")
-    region = aws.get_region().name
-    ami = region_to_ami.get(region, None)
-    if ami is None:
-        raise ArtifactException(f"Couldn't find an AMI for region {region}")
-    return ami
diff --git a/pulumi/infra/api.py b/pulumi/infra/api.py
index 926910f13b..c48b8da5ae 100644
--- a/pulumi/infra/api.py
+++ b/pulumi/infra/api.py
@@ -9,7 +9,6 @@
 from infra.graphql import GraphQL
 from infra.lambda_ import Lambda
 from infra.metric_forwarder import MetricForwarder
-from infra.model_plugin_deployer import ModelPluginDeployer
 from infra.network import Network
 from infra.secret import JWTSecret
 
@@ -214,15 +213,6 @@ def __init__(
 
         # These don't work in LocalStack for some reason
         if not LOCAL_GRAPL:
-            self.model_plugin_deployer = ModelPluginDeployer(
-                network=network,
-                db=db,
-                secret=secret,
-                plugins_bucket=plugins_bucket,
-                dgraph_cluster=dgraph_cluster,
-                forwarder=forwarder,
-            )
-
             self.graphql_endpoint = GraphQL(
                 network=network,
                 secret=secret,
@@ -238,10 +228,6 @@ def __init__(
         if not LOCAL_GRAPL:
             self.proxies.extend(
                 [
-                    self._add_proxy_resource_integration(
-                        self.model_plugin_deployer.function,
-                        path_part="modelPluginDeployer",
-                    ),
                     self._add_proxy_resource_integration(
                         self.graphql_endpoint.function, path_part="graphQlEndpoint"
                     ),
diff --git a/pulumi/infra/ec2_cluster.py b/pulumi/infra/ec2_cluster.py
deleted file mode 100644
index 44a03f5c2b..0000000000
--- a/pulumi/infra/ec2_cluster.py
+++ /dev/null
@@ -1,80 +0,0 @@
-from typing import Dict, List, Optional, Sequence
-
-import pulumi_aws as aws
-
-import pulumi
-from pulumi import Output
-
-
-class Ec2Cluster(pulumi.ComponentResource):
-    def __init__(
-        self,
-        name: str,
-        # For quorum purposes, you probably want something >=3 and odd.
-        num_instances: int,
-        ami: str,
-        instance_type: str,
-        iam_instance_profile: aws.iam.InstanceProfile,
-        vpc_private_subnet: aws.ec2.Subnet,
-        vpc_security_group_ids: Sequence[Output[str]],
-        instance_tags: Dict[str, str],
-        opts: Optional[pulumi.ResourceOptions] = None,
-    ) -> None:
-        super().__init__("grapl:Ec2ClusterResource", name=name, props=None, opts=opts)
-        child_opts = pulumi.ResourceOptions(parent=self)
-
-        self.instances = self._build_instances(
-            name=f"{name}-cluster",
-            ami=ami,
-            instance_type=instance_type,
-            num_instances=num_instances,
-            iam_instance_profile=iam_instance_profile,
-            vpc_private_subnet=vpc_private_subnet,
-            vpc_security_group_ids=vpc_security_group_ids,
-            instance_tags=instance_tags,
-            child_opts=child_opts,
-        )
-        self.register_outputs(
-            {
-                "instances": self.instances,
-            }
-        )
-
-    @staticmethod
-    def _build_instances(
-        name: str,
-        ami: str,
-        instance_type: str,
-        num_instances: int,
-        iam_instance_profile: aws.iam.InstanceProfile,
-        vpc_private_subnet: aws.ec2.Subnet,
-        vpc_security_group_ids: Sequence[Output[str]],
-        instance_tags: Dict[str, str],
-        child_opts: pulumi.ResourceOptions,
-    ) -> List[aws.ec2.Instance]:
-        instances = []
-        for i in range(0, num_instances):
-            instance_tags["Name"] = f"{name}-{i}"
-            instance = aws.ec2.Instance(
-                f"ec2-inst-{name}-{i}",
-                ami=ami,
-                instance_type=instance_type,
-                subnet_id=vpc_private_subnet.id,
-                vpc_security_group_ids=vpc_security_group_ids,
-                credit_specification=aws.ec2.InstanceCreditSpecificationArgs(
-                    cpu_credits="unlimited",
-                ),
-                tags=instance_tags,
-                iam_instance_profile=iam_instance_profile.name,
-                # Consul relies on EC2 metadata
-                metadata_options=aws.ec2.InstanceMetadataOptionsArgs(
-                    http_endpoint="enabled",
-                    # There's a chance we can change this to "required", which forces IMDSv2,
-                    # but we haven't tested that yet.
-                    http_tokens="optional",
-                ),
-                opts=child_opts,
-            )
-
-            instances.append(instance)
-        return instances
diff --git a/pulumi/infra/model_plugin_deployer.py b/pulumi/infra/model_plugin_deployer.py
deleted file mode 100644
index 3d6083e1c9..0000000000
--- a/pulumi/infra/model_plugin_deployer.py
+++ /dev/null
@@ -1,71 +0,0 @@
-from typing import Optional
-
-from infra import dynamodb
-from infra.bucket import Bucket
-from infra.config import LOCAL_GRAPL, configurable_envvars
-from infra.dgraph_cluster import DgraphCluster
-from infra.dynamodb import DynamoDB
-from infra.ec2 import Ec2Port
-from infra.lambda_ import Lambda, LambdaExecutionRole, LambdaResolver, PythonLambdaArgs
-from infra.metric_forwarder import MetricForwarder
-from infra.network import Network
-from infra.secret import JWTSecret
-
-import pulumi
-
-
-class ModelPluginDeployer(pulumi.ComponentResource):
-    def __init__(
-        self,
-        network: Network,
-        db: DynamoDB,
-        secret: JWTSecret,
-        plugins_bucket: Bucket,
-        dgraph_cluster: DgraphCluster,
-        forwarder: MetricForwarder,
-        opts: Optional[pulumi.ResourceOptions] = None,
-    ) -> None:
-
-        name = "model-plugin-deployer"
-        super().__init__("grapl:ModelPluginDeployer", name, None, opts)
-
-        self.role = LambdaExecutionRole(name, opts=pulumi.ResourceOptions(parent=self))
-
-        self.function = Lambda(
-            name,
-            args=PythonLambdaArgs(
-                execution_role=self.role,
-                handler="lambdex_handler.handler",
-                code=LambdaResolver.resolve(name),
-                env={
-                    **configurable_envvars(name, ["GRAPL_LOG_LEVEL"]),
-                    "GRAPL_MODEL_PLUGINS_BUCKET": plugins_bucket.bucket,
-                    "MG_ALPHAS": dgraph_cluster.alpha_host_port,
-                    "JWT_SECRET_ID": secret.secret.arn
-                    if not LOCAL_GRAPL
-                    else "JWT_SECRET_ID",
-                    "GRAPL_SCHEMA_PROPERTIES_TABLE": db.schema_properties_table.id,
-                    "GRAPL_SCHEMA_TABLE": db.schema_table.id,
-                },
-                timeout=25,
-                memory_size=256,
-            ),
-            network=network,
-            opts=pulumi.ResourceOptions(parent=self),
-        )
-
-        Ec2Port("tcp", 443).allow_outbound_any_ip(self.function.security_group)
-
-        forwarder.subscribe_to_log_group(name, self.function.log_group)
-
-        secret.grant_read_permissions_to(self.role)
-
-        dynamodb.grant_read_write_on_tables(
-            self.role, [db.schema_table, db.schema_properties_table]
-        )
-
-        plugins_bucket.grant_read_write_permissions_to(self.role)
-
-        dgraph_cluster.allow_connections_from(self.function.security_group)
-
-        self.register_outputs({})
diff --git a/pulumi/infra/nomad_cluster.py b/pulumi/infra/nomad_cluster.py
deleted file mode 100644
index 37a20d1732..0000000000
--- a/pulumi/infra/nomad_cluster.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from typing import Optional
-
-from infra.network import Network
-from infra.nomad_server_fleet import NomadServerFleet
-
-import pulumi
-
-
-class NomadCluster(pulumi.ComponentResource):
-    def __init__(
-        self,
-        name: str,
-        network: Network,
-        opts: Optional[pulumi.ResourceOptions] = None,
-    ) -> None:
-        super().__init__("grapl:NomadCluster", name=name, props=None, opts=opts)
-        child_opts = pulumi.ResourceOptions(parent=self)
-
-        self.nomad_server_fleet = NomadServerFleet(
-            "nomad-server-fleet",
-            network=network,
-            internal_service_ports=(),
-            opts=child_opts,
-        )
-
-        # Theoretically in the future, we'll also have a `self.nomad_client_fleet =`
diff --git a/pulumi/infra/nomad_server_fleet.py b/pulumi/infra/nomad_server_fleet.py
deleted file mode 100644
index 13b253baa7..0000000000
--- a/pulumi/infra/nomad_server_fleet.py
+++ /dev/null
@@ -1,172 +0,0 @@
-import json
-from typing import Optional, Sequence
-
-import pulumi_aws as aws
-from infra import policies
-from infra.ami_artifacts import get_ami_id
-from infra.config import DEPLOYMENT_NAME, DGRAPH_LOG_RETENTION_DAYS
-from infra.ec2 import Ec2Port
-from infra.ec2_cluster import Ec2Cluster
-from infra.network import Network
-from infra.policies import EC2_DESCRIBE_INSTANCES_POLICY
-
-import pulumi
-
-
-class NomadServerFleet(pulumi.ComponentResource):
-    """
-    NOTE: We also currently bake in the Consul server into the Nomad server;
-    they are colocated.
-    The image is defined in `image.pkr.hcl` and is based on:
-    https://github.com/hashicorp/terraform-aws-nomad/tree/master/examples/nomad-consul-ami
-
-    This decision can be revisited, in which case you'd want to look at
-    https://github.com/hashicorp/terraform-aws-nomad/tree/master/examples/nomad-consul-separate-cluster
-    """
-
-    def __init__(
-        self,
-        name: str,
-        network: Network,
-        internal_service_ports: Sequence[Ec2Port],
-        opts: Optional[pulumi.ResourceOptions] = None,
-    ) -> None:
-        super().__init__("grapl:NomadServerFleet", name=name, props=None, opts=opts)
-        child_opts = pulumi.ResourceOptions(parent=self)
-        vpc = network.vpc
-
-        self.log_group = aws.cloudwatch.LogGroup(
-            f"{name}-logs",
-            retention_in_days=DGRAPH_LOG_RETENTION_DAYS,  # TODO: ??
-            opts=child_opts,
-        )
-
-        self.security_group = aws.ec2.SecurityGroup(
-            f"{name}-sec-group",
-            description=f"Nomad server security group",
-            vpc_id=vpc.id,
-            opts=child_opts,
-        )
-
-        self.role = aws.iam.Role(
-            f"{name}-role",
-            description="IAM role for Nomad server instances",
-            assume_role_policy=json.dumps(
-                {
-                    "Version": "2012-10-17",
-                    "Statement": [
-                        {
-                            "Action": "sts:AssumeRole",
-                            "Effect": "Allow",
-                            "Principal": {
-                                "Service": "ec2.amazonaws.com",
-                            },
-                        }
-                    ],
-                }
-            ),
-            opts=child_opts,
-        )
-
-        self._open_initial_ports(internal_service_ports)
-
-        # InstanceProfile for nomad-server instances
-        instance_profile = aws.iam.InstanceProfile(
-            f"{name}-instance-profile",
-            opts=child_opts,
-            role=self.role.name,
-            name=f"{DEPLOYMENT_NAME}-nomad-server-instance-profile",
-        )
-
-        # CloudWatchAgentServerPolicy allows the Nomad server instances to
-        # run the CloudWatch Agent.
-        policies.attach_policy(
-            role=self.role, policy=policies.CLOUDWATCH_AGENT_SERVER_POLICY
-        )
-        policies.attach_policy(role=self.role, policy=policies.SSM_POLICY)
-
-        policies.attach_policy(role=self.role, policy=EC2_DESCRIBE_INSTANCES_POLICY)
-
-        nomad_server_ami = get_ami_id("grapl-nomad-consul-server")
-
-        # When we decide to do multiple clusters,
-        # make sure to spread out the private subnets they're attached to.
-        nomad_servers = Ec2Cluster(
-            name="nomad-servers",
-            ami=nomad_server_ami,
-            num_instances=3,
-            instance_type="t2.micro",
-            iam_instance_profile=instance_profile,
-            vpc_private_subnet=network.private_subnets[0],
-            vpc_security_group_ids=[self.security_group.id],
-            instance_tags={
-                "nomad-server-sec-group-for-deployment": DEPLOYMENT_NAME,
-                "ConsulAgent": "Server",
-            },
-            opts=child_opts,
-        )
-
-    def _open_initial_ports(self, internal_service_ports: Sequence[Ec2Port]) -> None:
-        # allow hosts in the nomad-server security group to communicate
-        # internally on the following ports:
-        # https://www.nomadproject.io/docs/install/production/requirements#ports-used
-        # https://www.consul.io/docs/install/ports
-        for port in (
-            # Nomad ports
-            Ec2Port("tcp", 4646),
-            Ec2Port("tcp", 4647),
-            Ec2Port("tcp", 4648),
-            Ec2Port("udp", 4648),
-            # Consul ports
-            Ec2Port("tcp", 8300),
-            Ec2Port("tcp", 8301),
-            Ec2Port("udp", 8301),
-            Ec2Port("tcp", 8302),
-            Ec2Port("udp", 8302),
-            Ec2Port("tcp", 8500),
-            Ec2Port("udp", 8600),
-            Ec2Port("tcp", 8600),
-            *internal_service_ports,
-        ):
-            port.allow_internally(self.security_group)
-
-    def allow_connections_from(
-        self,
-        other: aws.ec2.SecurityGroup,
-        port_range: Ec2Port,
-    ) -> None:
-        descriptor = "-".join(
-            [
-                "from",
-                other._name,
-                "for",
-                str(port_range),
-            ]
-        )
-
-        # We'll accept connections from Other into SecurityGroup
-        aws.ec2.SecurityGroupRule(
-            f"ingress-{descriptor}",
-            type="ingress",
-            source_security_group_id=other.id,
-            security_group_id=self.security_group.id,
-            from_port=port_range.port,
-            to_port=port_range.port,
-            protocol=port_range.protocol,
-            opts=pulumi.ResourceOptions(parent=self.security_group),
-        )
-
-        # Allow connections out of Other to Self
-        aws.ec2.SecurityGroupRule(
-            f"egress-{descriptor}",
-            type="egress",
-            # Awful naming scheme
-            # https://grapl-internal.slack.com/archives/C017DKYF55H/p1621459760032000
-            # For egress, `source` is where it's going
-            source_security_group_id=self.security_group.id,
-            security_group_id=other.id,
-            from_port=port_range.port,
-            to_port=port_range.port,
-            protocol=port_range.protocol,
-            opts=pulumi.ResourceOptions(parent=self.security_group),
-        )
diff --git a/pulumi/policies/3rdparty/awsguard/BUILD b/pulumi/policies/3rdparty/awsguard/BUILD
deleted file mode 100644
index 92187c86a5..0000000000
--- a/pulumi/policies/3rdparty/awsguard/BUILD
+++ /dev/null
@@ -1,1 +0,0 @@
-shell_library()
diff --git a/src/js/engagement_view/BUILD b/src/js/engagement_view/BUILD
deleted file mode 100644
index 92187c86a5..0000000000
--- a/src/js/engagement_view/BUILD
+++ /dev/null
@@ -1,1 +0,0 @@
-shell_library()
diff --git a/src/js/engagement_view/Makefile b/src/js/engagement_view/Makefile
index 5eed740b70..7bc01ff34e 100644
--- a/src/js/engagement_view/Makefile
+++ b/src/js/engagement_view/Makefile
@@ -22,6 +22,7 @@ node_modules: package.json yarn.lock
 	$(DOCKER_RUN) yarn install
 
 build: node_modules $(ASSET_DIRS) $(ASSET_FILES)
+	echo "--- Building Engagement View"
 	$(DOCKER_RUN) yarn build
 
 .PHONY: test
diff --git a/src/proto/google/protobuf/wrappers.proto b/src/proto/google/protobuf/wrappers.proto
deleted file mode 100644
index c5632e5ca9..0000000000
--- a/src/proto/google/protobuf/wrappers.proto
+++ /dev/null
@@ -1,118 +0,0 @@
-// Protocol Buffers - Google's data interchange format
-// Copyright 2008 Google Inc.  All rights reserved.
-// https://developers.google.com/protocol-buffers/
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-//     * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-//     * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-//     * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-// Wrappers for primitive (non-message) types. These types are useful
-// for embedding primitives in the `google.protobuf.Any` type and for places
-// where we need to distinguish between the absence of a primitive
-// typed field and its default value.
-
-syntax = "proto3";
-
-package google.protobuf;
-
-option csharp_namespace = "Google.Protobuf.WellKnownTypes";
-option cc_enable_arenas = true;
-option go_package = "types";
-option java_package = "com.google.protobuf";
-option java_outer_classname = "WrappersProto";
-option java_multiple_files = true;
-option objc_class_prefix = "GPB";
-
-// Wrapper message for `double`.
-//
-// The JSON representation for `DoubleValue` is JSON number.
-message DoubleValue {
-  // The double value.
-  double value = 1;
-}
-
-// Wrapper message for `float`.
-//
-// The JSON representation for `FloatValue` is JSON number.
-message FloatValue {
-  // The float value.
-  float value = 1;
-}
-
-// Wrapper message for `int64`.
-//
-// The JSON representation for `Int64Value` is JSON string.
-message Int64Value {
-  // The int64 value.
-  int64 value = 1;
-}
-
-// Wrapper message for `uint64`.
-//
-// The JSON representation for `UInt64Value` is JSON string.
-message UInt64Value {
-  // The uint64 value.
-  uint64 value = 1;
-}
-
-// Wrapper message for `int32`.
-//
-// The JSON representation for `Int32Value` is JSON number.
-message Int32Value {
-  // The int32 value.
-  int32 value = 1;
-}
-
-// Wrapper message for `uint32`.
-//
-// The JSON representation for `UInt32Value` is JSON number.
-message UInt32Value {
-  // The uint32 value.
-  uint32 value = 1;
-}
-
-// Wrapper message for `bool`.
-//
-// The JSON representation for `BoolValue` is JSON `true` and `false`.
-message BoolValue {
-  // The bool value.
-  bool value = 1;
-}
-
-// Wrapper message for `string`.
-//
-// The JSON representation for `StringValue` is JSON string.
-message StringValue {
-  // The string value.
-  string value = 1;
-}
-
-// Wrapper message for `bytes`.
-//
-// The JSON representation for `BytesValue` is JSON string.
-message BytesValue {
-  // The bytes value.
-  bytes value = 1;
-}
diff --git a/src/proto/graplinc/grapl/api/BUILD b/src/proto/graplinc/grapl/api/BUILD
deleted file mode 100644
index 17a34144b1..0000000000
--- a/src/proto/graplinc/grapl/api/BUILD
+++ /dev/null
@@ -1,8 +0,0 @@
-protobuf_library(
-    # Since we distribute generated code for Python at this level, it
-    # makes sense to also collect the sources for all subdirectories
-    # at this level, rather than specifying BUILD files in each
-    # subdirectory.
-    sources=["**/*.proto"],
-    python_source_root="src/python/graplinc"
-)
diff --git a/src/proto/graplinc/grapl/api/graph/v1beta1/types.proto b/src/proto/graplinc/grapl/api/graph/v1beta1/types.proto
index 3e7801be6c..ef8358d3b3 100644
--- a/src/proto/graplinc/grapl/api/graph/v1beta1/types.proto
+++ b/src/proto/graplinc/grapl/api/graph/v1beta1/types.proto
@@ -1,7 +1,5 @@
 syntax = "proto3";
 
-import "google/protobuf/wrappers.proto";
-
 package graplinc.grapl.api.graph.v1beta1;
 
 // The Session Identity Strategy is used to derive a canonical identity for any node that has a
diff --git a/src/proto/graplinc/grapl/metrics/v1/metric_types.proto b/src/proto/graplinc/grapl/metrics/v1/metric_types.proto
index 196cdeb653..bfcab1fba7 100644
--- a/src/proto/graplinc/grapl/metrics/v1/metric_types.proto
+++ b/src/proto/graplinc/grapl/metrics/v1/metric_types.proto
@@ -31,7 +31,7 @@ message Gauge {
         // Protobuf makes me specify a 0 value, it's invalid
         GAUGE_TYPE_UNSPECIFIED = 0;
         // Sets the value of the gauge to this value.
-        GAUGE_TYPE_ABSOLUTE =1;
+        GAUGE_TYPE_ABSOLUTE = 1;
         // Increments the value of the gauge by this much.
         GAUGE_TYPE_INCREMENT = 2;
         // Decrements the value of the gauge by this much.
diff --git a/src/proto/graplinc/grapl/pipeline/v1beta1/types.proto b/src/proto/graplinc/grapl/pipeline/v1beta1/types.proto
new file mode 100644
index 0000000000..7d5455b60c
--- /dev/null
+++ b/src/proto/graplinc/grapl/pipeline/v1beta1/types.proto
@@ -0,0 +1,39 @@
+syntax = "proto3";
+
+package graplinc.grapl.pipeline.v1beta1;
+
+// An RFC 4122 UUID, encoded by splitting the 128 bit representation into two 64
+// bit values
+message Uuid {
+  // Least significant bytes
+  fixed64 lsb = 1;
+
+  // Most significant bytes
+  fixed64 msb = 2;
+}
+
+// Metadata about a specific message, or its context
+message Metadata {
+  // A tenant (e.g. a customer) in Grapl
+  Uuid tenant_id = 1;
+
+  // An operation (e.g. a request) across Grapl services
+  Uuid trace_id = 2;
+
+  // Number of times this message has been retried
+  uint32 retry_count = 3;
+}
+
+// A wrapper message for pipeline data. Includes useful metadata to help
+// services determine what they should do with the inner_message.
+message Envelope {
+  // Metadata about this message
+  Metadata metadata = 1;
+
+  // Fully-qualified name of the protobuf type used to serialize inner_message
+  //   ex: "graplinc.grapl.api.graph.v1beta1.GraphDescription"
+  string inner_type = 2;
+
+  // The serialized bytes of the inner message
+  bytes inner_message = 3;
+}
diff --git a/src/python/Dockerfile b/src/python/Dockerfile
index 4a4f1098d0..9d5721cb14 100644
--- a/src/python/Dockerfile
+++ b/src/python/Dockerfile
@@ -1,240 +1,71 @@
-# base build
+# grapl-python-base
 ################################################################################
-
-FROM python:3.7-slim-buster AS grapl-python-build
-
-SHELL ["/bin/bash", "-c"]
-
-RUN apt-get update && apt-get -y install --no-install-recommends \
-        build-essential \
-        protobuf-compiler \
-        zip \
-    && rm -rf /var/lib/apt/lists/*
-
-ENV PROTOC /usr/bin/protoc
-ENV PROTOC_INCLUDE /usr/include
-
-RUN adduser \
-        --disabled-password \
-        --gecos '' \
-        --home /home/grapl \
-        --shell /bin/bash \
-        grapl
-
-USER grapl
-ENV USER=grapl
-WORKDIR /home/grapl
-
-# Automatically ensures that our virtualenv is created and active on
-# all subsequent actions
-ENV VIRTUAL_ENV=/home/grapl/venv
-RUN python3 -m venv $VIRTUAL_ENV
-ENV PATH="$VIRTUAL_ENV/bin:$PATH"
-
-RUN pip install --upgrade pip && \
-    pip install wheel grpcio chalice hypothesis pytest pytest-xdist
-
-# base deploy
-################################################################################
-
-FROM python:3.7-slim-buster AS grapl-python-deploy
-
+FROM python:3.7-slim-buster AS grapl-python-base
 SHELL ["/bin/bash", "-c"]
-
 RUN apt-get update && apt-get -y install --no-install-recommends \
         bash \
         libstdc++6 \
     && rm -rf /var/lib/apt/lists/*
-
 RUN adduser \
         --disabled-password \
         --gecos '' \
         --home /home/grapl \
         --shell /bin/bash \
         grapl
-
 USER grapl
 WORKDIR /home/grapl
-
-# Automatically ensures that our virtualenv is created and active on
-# all subsequent actions!
-ENV VIRTUAL_ENV=/home/grapl/venv
-RUN python3 -m venv $VIRTUAL_ENV
-ENV PATH="$VIRTUAL_ENV/bin:$PATH"
-
-RUN pip install --upgrade pip
-
-# test deps
-################################################################################
-
-FROM grapl-python-build AS python-test-deps
-
-COPY --chown=grapl src/python/python_test_deps python_test_deps
-
-RUN python_test_deps/download_requirements.sh
-
-
-
-# graplinc-grapl-api
-################################################################################
-
-# build
-FROM grapl-python-build AS graplinc-grapl-api-build
-
-COPY --chown=grapl src/proto proto
-COPY --chown=grapl src/python/graplinc-grapl-api python/graplinc-grapl-api
-
-RUN cd python/graplinc-grapl-api && \
-    python setup.py sdist bdist_wheel && \
-    pip install dist/*.whl
-
-# grapl_analyzerlib
-################################################################################
-
-# build
-FROM grapl-python-build AS grapl-analyzerlib-build
-
-COPY --chown=grapl src/python/grapl_analyzerlib grapl_analyzerlib
-COPY --chown=grapl --from=graplinc-grapl-api-build /home/grapl/venv venv
-
-RUN --mount=type=bind,source=dist,target=/grapl-dist cd /grapl-dist && \
-    pip install grapl_common-*.whl
-
-RUN cd grapl_analyzerlib && \
-    pip install . && \
-    python setup.py sdist bdist_wheel
-
-# test
-FROM grapl-analyzerlib-build AS grapl-analyzerlib-test
-
-## Install test requirements
-COPY --chown=grapl --from=python-test-deps /home/grapl/python_test_deps python_test_deps
-RUN python_test_deps/install_requirements.sh
-
-# engagement-creator (this will die when we merge data-tenancy from main into nomad)
-################################################################################
-
-# build
-FROM grapl-python-build AS engagement-creator-build
-
-COPY --chown=grapl src/python/engagement-creator engagement-creator
-COPY --chown=grapl --from=grapl-analyzerlib-build /home/grapl/venv venv
-
-RUN cd engagement-creator && \
-    pip install .
-
-# deploy
-FROM grapl-python-deploy AS engagement-creator-deploy
-
-COPY --chown=grapl --from=engagement-creator-build /home/grapl/venv venv
-COPY --chown=grapl --from=engagement-creator-build /home/grapl/engagement-creator engagement-creator
-
-# Awful, stupid hacks around Python packaging.
-# But this will go away in like 2 days when I merge in data-tenancy.
-WORKDIR /home/grapl/engagement-creator
-RUN mv engagement_creator/run.py ./
-CMD python3 ./run.py
-
+ENTRYPOINT ["/bin/bash", "-o", "errexit", "-o", "nounset", "-c"]
 
 # analyzer-executor
 ################################################################################
+FROM grapl-python-base AS analyzer-executor
+COPY ./dist/analyzer-executor.pex /home/grapl/analyzer-executor.pex
+CMD ["./analyzer-executor.pex"]
 
-# build
-FROM grapl-python-build AS analyzer-executor-build
-
-COPY --chown=grapl src/python/analyzer_executor analyzer_executor
-COPY --chown=grapl --from=grapl-analyzerlib-build /home/grapl/venv venv
-
-RUN cd analyzer_executor && \
-    pip install .
-
-# deploy
-FROM grapl-python-deploy AS analyzer-executor-deploy
-
-COPY --chown=grapl --from=analyzer-executor-build /home/grapl/venv venv
-COPY --chown=grapl --from=analyzer-executor-build /home/grapl/analyzer_executor analyzer_executor
-
-CMD python3 analyzer_executor/src/run.py
-
-# test
-FROM analyzer-executor-build AS analyzer-executor-test
-
-ENV IS_RETRY=False
-
-## Install test requirements
-COPY --chown=grapl --from=python-test-deps /home/grapl/python_test_deps python_test_deps
-RUN python_test_deps/install_requirements.sh
-
-# model-plugin-deployer
+# engagement-creator
 ################################################################################
+FROM grapl-python-base AS engagement-creator
+COPY ./dist/engagement-creator.pex /home/grapl/engagement-creator.pex
+CMD ["./engagement-creator.pex"]
 
-# build
-FROM grapl-python-build AS model-plugin-deployer-build
-
-COPY --chown=grapl src/python/grapl-model-plugin-deployer model-plugin-deployer
-COPY --chown=grapl --from=grapl-analyzerlib-build /home/grapl/venv venv
-
-RUN cd model-plugin-deployer && \
-    pip install -r requirements.txt
-
-# deploy
-FROM grapl-python-deploy AS model-plugin-deployer-deploy
-
-COPY --chown=grapl --from=model-plugin-deployer-build /home/grapl/venv venv
-
-RUN chalice new-project app/
-
-COPY --chown=grapl --from=model-plugin-deployer-build /home/grapl/model-plugin-deployer/grapl_model_plugin_deployer.py app/app.py
-
-CMD cd app && \
-    chalice local --no-autoreload --host=0.0.0.0 --port=8123
 
-# Notebook
+# grapl-notebook
 ################################################################################
-
-# build
-FROM grapl-python-build AS grapl-notebook
-
-SHELL ["/bin/bash", "-o", "pipefail", "-c"]
+FROM grapl-python-base AS grapl-notebook
 EXPOSE 8888
-
-COPY --chown=grapl --from=grapl-analyzerlib-build /home/grapl/venv venv
-
 RUN pip install jupyter
-
-# Set up jupyter-notebook stuff
 RUN mkdir -p grapl-notebook/model_plugins
 COPY --chown=grapl src/python/grapl-notebook/jupyter_notebook_config.py /home/grapl/.jupyter/
-COPY --chown=grapl src/python/grapl-notebook/Demo_Engagement.ipynb grapl-notebook/
-
-## Run it
-CMD cd grapl-notebook && \
-    jupyter notebook --ip="0.0.0.0"
-
-# test
-FROM grapl-notebook AS grapl-notebook-test
+COPY --chown=grapl src/python/grapl-notebook/Demo_Engagement.ipynb /home/grapl/grapl-notebook/
+WORKDIR /home/grapl/grapl-notebook
+CMD ["jupyter", "notebook", "--ip", "0.0.0.0"]
 
-# This file will improve once #444 lands
-# primarily to use pre-downloaded tools instead of downloading again
-
-RUN pip install nbqa mypy boto3-stubs[essential]
-
-# grapl-tests-common-build
+# python-integration-tests
 ################################################################################
-FROM grapl-python-build AS grapl-tests-common-build
-
-COPY --chown=grapl --from=grapl-analyzerlib-build /home/grapl/venv venv
-COPY --from=python-test-deps /home/grapl/python_test_deps python_test_deps
-
-RUN python_test_deps/install_requirements.sh
-
-COPY --chown=grapl src/python/grapl-tests-common grapl-tests-common
-RUN cd grapl-tests-common && \
-    pip install . && \
-    python setup.py sdist bdist_wheel
-
-# GraphQL Endpoint tests
-################################################################################
-FROM grapl-tests-common-build AS graphql-endpoint-tests
-
-COPY --chown=grapl src/python/graphql_endpoint_tests graphql_endpoint_tests
+FROM grapl-python-base AS python-integration-tests
+USER root
+RUN apt-get update && apt-get -y install --no-install-recommends \
+        build-essential \
+        protobuf-compiler \
+        curl \
+        unzip \
+    && rm -rf /var/lib/apt/lists/*
+RUN apt-get update && apt-get -y install --no-install-recommends curl unzip
+RUN adduser \
+        --disabled-password \
+        --gecos '' \
+        --home /home/grapl \
+        --shell /bin/bash \
+        --uid 2000 \
+        pants_ci
+RUN mkdir -p /tmp/pants && chmod -R 777 /tmp/pants
+USER grapl
+RUN mkdir -p /tmp/pants/named_caches && chmod -R 777 /tmp/pants/named_caches
+RUN mkdir -p /tmp/pants/lmdb_store && chmod -R 777 /tmp/pants/lmdb_store
+RUN mkdir -p /tmp/pants/setup && chmod -R 777 /tmp/pants/setup
+ENV PANTS_NAMED_CACHES_DIR=/tmp/pants/named_caches
+ENV PANTS_LOCAL_STORE_DIR=/tmp/pants/lmdb_store
+ENV PANTS_SETUP_CACHE=/tmp/pants/setup
+
+# grapl-root is mounted by Nomad.
+ENTRYPOINT ["/bin/bash", "-o", "errexit", "-o", "nounset", "-c", "cd /mnt/grapl-root && ./pants filter --filter-target-type='python_tests' :: | xargs ./pants --tag='-needs_work' test --pytest-args='-m integration_test'"]
\ No newline at end of file
diff --git a/src/python/analyzer_executor/requirements.txt b/src/python/analyzer_executor/requirements.txt
deleted file mode 100644
index bd36ecb844..0000000000
--- a/src/python/analyzer_executor/requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-pydgraph==20.7.0
-boto3
-grapl-analyzerlib
-redis
-grpcio
-protobuf
-typing_extensions
-hypothesis
-grapl-common
diff --git a/src/python/analyzer_executor/setup.py b/src/python/analyzer_executor/setup.py
deleted file mode 100644
index 5b3b2fef41..0000000000
--- a/src/python/analyzer_executor/setup.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Setuptools script for Grapl analyzer_executor"""
-
-import os
-
-from setuptools import find_packages, setup  # type: ignore
-
-HERE = os.path.abspath(os.path.dirname(__file__))
-
-
-def is_comment(line):
-    """check whether a line is a comment"""
-    return line.strip().startswith("#")
-
-
-with open(os.path.join(HERE, "requirements.txt")) as requirements:
-    REQUIREMENTS = tuple(line.strip() for line in requirements if not is_comment(line))
-
-__version__ = "1.0.0"
-
-setup(
-    name="analyzer_executor",
-    version=__version__,
-    author="Grapl, Inc.",
-    author_email="grapl.code@graplsecurity.com",
-    url="https://github.com/grapl-security/grapl",
-    description="Grapl service which runs analyzers on the graph",
-    packages=find_packages(),
-    install_requires=REQUIREMENTS,
-    zip_safe=False,
-)
diff --git a/src/python/analyzer_executor/src/BUILD b/src/python/analyzer_executor/src/BUILD
index d80ecb5983..ceb3cd1e85 100644
--- a/src/python/analyzer_executor/src/BUILD
+++ b/src/python/analyzer_executor/src/BUILD
@@ -1,1 +1,13 @@
-python_library()
+python_library(
+    name="analyzer_executor",
+    dependencies=[
+        "./analyzer_executor_lib",
+        "./plugins",
+    ]
+)
+
+pex_binary(
+    output_path="analyzer-executor.pex",
+    entry_point="./run.py:run",
+    execution_mode="venv",
+)
diff --git a/src/python/analyzer_executor/src/analyzer_executor_lib/analyzer_executor.py b/src/python/analyzer_executor/src/analyzer_executor_lib/analyzer_executor.py
index f5c34f3c40..1390f0d3a2 100644
--- a/src/python/analyzer_executor/src/analyzer_executor_lib/analyzer_executor.py
+++ b/src/python/analyzer_executor/src/analyzer_executor_lib/analyzer_executor.py
@@ -26,6 +26,7 @@
 )
 
 import boto3
+import grapl_analyzerlib.counters  # noqa: F401
 from analyzer_executor_lib.redis_cache import EitherCache, construct_redis_client
 from grapl_analyzerlib.analyzer import Analyzer
 from grapl_analyzerlib.execution import ExecutionComplete, ExecutionFailed, ExecutionHit
@@ -38,6 +39,7 @@
 from grapl_common.grapl_logger import get_module_grapl_logger
 from grapl_common.metrics.metric_reporter import MetricReporter, TagPair
 from grapl_common.sqs.sqs_types import S3PutRecordDict, SQSMessageBody
+from python_proto.pipeline import Envelope, Metadata
 
 if TYPE_CHECKING:
     from mypy_boto3_s3 import S3ServiceResource
@@ -188,7 +190,10 @@ async def handle_events(self, events: SQSMessageBody, context: Any) -> None:
         for event in events["Records"]:
             data = parse_s3_event(s3, event)
 
-            message = json.loads(data)
+            envelope = Envelope.deserialize(data)
+            # Keep in mind that, today, we don't communicate this message via protobuf but instead
+            # we use JSON. It would be worth changing this in another issue.
+            message = json.loads(envelope.inner_message)
 
             LOGGER.info(f'Executing Analyzer: {message["key"]}')
 
@@ -199,13 +204,13 @@ async def handle_events(self, events: SQSMessageBody, context: Any) -> None:
                     s3,
                     self.analyzers_bucket,
                     message["key"],
-                )
+                ).decode("utf8")
             analyzer_name = message["key"].split("/")[-2]
 
             subgraph = SubgraphView.from_proto(client, bytes(message["subgraph"]))
 
             # TODO: Validate signature of S3 file
-            LOGGER.info(f"event {event}")
+            LOGGER.info(f"event {event} {envelope.metadata}")
             rx: Connection
             tx: Connection
             rx, tx = Pipe(duplex=False)
@@ -221,7 +226,12 @@ async def handle_events(self, events: SQSMessageBody, context: Any) -> None:
                     "analyzer-executor.emit_event.ms",
                     (TagPair("analyzer_name", exec_hit.analyzer_name),),
                 ):
-                    emit_event(self.analyzer_matched_subgraphs_bucket, s3, exec_hit)
+                    emit_event(
+                        self.analyzer_matched_subgraphs_bucket,
+                        s3,
+                        exec_hit,
+                        envelope.metadata,
+                    )
                 self.update_msg_cache(analyzer, exec_hit.root_node_key, message["key"])
                 self.update_hit_cache(analyzer_name, exec_hit.root_node_key)
 
@@ -370,7 +380,7 @@ def exec_analyzer(
             raise
 
 
-def parse_s3_event(s3: S3ServiceResource, event: S3PutRecordDict) -> str:
+def parse_s3_event(s3: S3ServiceResource, event: S3PutRecordDict) -> bytes:
     try:
         bucket = event["s3"]["bucket"]["name"]
         key = event["s3"]["object"]["key"]
@@ -380,9 +390,9 @@ def parse_s3_event(s3: S3ServiceResource, event: S3PutRecordDict) -> str:
     return download_s3_file(s3, bucket, key)
 
 
-def download_s3_file(s3: S3ServiceResource, bucket: str, key: str) -> str:
+def download_s3_file(s3: S3ServiceResource, bucket: str, key: str) -> bytes:
     obj = s3.Object(bucket, key)
-    return cast(bytes, obj.get()["Body"].read()).decode("utf-8")
+    return cast(bytes, obj.get()["Body"].read())
 
 
 def is_analyzer(analyzer_name: str, analyzer_cls: type) -> bool:
@@ -409,10 +419,17 @@ def chunker(seq: List[BaseView], size: int) -> List[List[BaseView]]:
 
 
 def emit_event(
-    analyzer_matched_subgraphs_bucket: str, s3: S3ServiceResource, event: ExecutionHit
+    analyzer_matched_subgraphs_bucket: str,
+    s3: S3ServiceResource,
+    event: ExecutionHit,
+    metadata: Metadata,
 ) -> None:
     LOGGER.info(f"emitting event for: {event.analyzer_name, event.nodes}")
 
+    meta_dict = {
+        "trace_id": str(metadata.trace_id),
+    }
+
     event_s = json.dumps(
         {
             "nodes": json.loads(event.nodes),
@@ -421,6 +438,7 @@ def emit_event(
             "risk_score": event.risk_score,
             "lenses": event.lenses,
             "risky_node_keys": event.risky_node_keys,
+            "metadata": meta_dict,
         }
     )
     event_hash = hashlib.sha256(event_s.encode())
diff --git a/src/python/analyzer_executor/src/run.py b/src/python/analyzer_executor/src/run.py
index a9b0690fc5..5a12cb1c6d 100644
--- a/src/python/analyzer_executor/src/run.py
+++ b/src/python/analyzer_executor/src/run.py
@@ -17,7 +17,7 @@
 LOGGER = get_module_grapl_logger()
 
 
-async def main() -> None:
+async def _main() -> None:
     """
     Some TODOs to bring this inline with sqs-executor in Rust:
     - add the shortcut-to-DEAD_LETTER_QUEUE_URL case
@@ -47,4 +47,9 @@ async def main() -> None:
             LOGGER.error("SQS Timeout Manager exception", exc_info=True)
 
 
-asyncio.run(main())
+def run() -> None:
+    asyncio.run(_main())
+
+
+if __name__ == "__main__":
+    run()
diff --git a/src/python/engagement-creator/engagement_creator/BUILD b/src/python/engagement-creator/engagement_creator/BUILD
index 830f92f649..8862b9c2dc 100644
--- a/src/python/engagement-creator/engagement_creator/BUILD
+++ b/src/python/engagement-creator/engagement_creator/BUILD
@@ -1,1 +1,9 @@
-python_library()
\ No newline at end of file
+python_library()
+
+pex_binary(
+    name="pex",
+    output_path="engagement-creator.pex",
+    entry_point="./run.py:run",
+    execution_mode="venv",
+)
+
diff --git a/src/python/engagement-creator/engagement_creator/run.py b/src/python/engagement-creator/engagement_creator/run.py
index c33fd7ef4a..927bac0b7f 100644
--- a/src/python/engagement-creator/engagement_creator/run.py
+++ b/src/python/engagement-creator/engagement_creator/run.py
@@ -14,7 +14,7 @@
 LOGGER = get_module_grapl_logger()
 
 
-async def main() -> None:
+async def _main() -> None:
     """
     Some TODOs to bring this inline with sqs-executor in Rust:
     - add the shortcut-to-DEAD_LETTER_QUEUE_URL case
@@ -42,4 +42,9 @@ async def main() -> None:
             LOGGER.error("SQS Timeout Manager exception", exc_info=True)
 
 
-asyncio.run(main())
+def run() -> None:
+    asyncio.run(_main())
+
+
+if __name__ == "__main__":
+    run()
diff --git a/src/python/engagement-creator/requirements.txt b/src/python/engagement-creator/requirements.txt
deleted file mode 100644
index 82bd30ab5a..0000000000
--- a/src/python/engagement-creator/requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-pydgraph==20.7.*
-boto3
-boto3-stubs[essential]
-grapl-analyzerlib
-grapl-common
diff --git a/src/python/engagement-creator/setup.py b/src/python/engagement-creator/setup.py
deleted file mode 100644
index 4ade16f053..0000000000
--- a/src/python/engagement-creator/setup.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# type: ignore
-"""Setuptools script for Grapl engagement_creator"""
-
-import os
-
-from setuptools import setup
-
-HERE = os.path.abspath(os.path.dirname(__file__))
-
-
-def is_comment(line):
-    """check whether a line is a comment"""
-    return line.strip().startswith("#")
-
-
-with open(os.path.join(HERE, "requirements.txt")) as requirements:
-    REQUIREMENTS = tuple(line.strip() for line in requirements if not is_comment(line))
-
-__version__ = "1.0.0"
-
-setup(
-    name="engagement_creator",
-    version=__version__,
-    author="Grapl, Inc.",
-    author_email="grapl.code@graplsecurity.com",
-    url="https://github.com/grapl-security/grapl",
-    description="Grapl service which creates engagements",
-    install_requires=REQUIREMENTS,
-    setup_requires=("wheel",),
-    zip_safe=False,
-    extras_require={
-        "typecheck": [
-            "mypy",
-        ]
-    },
-)
diff --git a/src/python/grapl-dgraph-ttl/requirements.txt b/src/python/grapl-dgraph-ttl/requirements.txt
deleted file mode 100644
index e6f8f8eb11..0000000000
--- a/src/python/grapl-dgraph-ttl/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-pydgraph==20.7.0
-grapl_analyzerlib
-chalice
diff --git a/src/python/grapl-model-plugin-deployer/requirements.txt b/src/python/grapl-model-plugin-deployer/requirements.txt
deleted file mode 100644
index cec2940809..0000000000
--- a/src/python/grapl-model-plugin-deployer/requirements.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-pydgraph==20.7.0
-boto3
-pyjwt
-grapl_analyzerlib
-chalice==1.22.4
-pygithub
-grapl-common
\ No newline at end of file
diff --git a/src/python/grapl_analyzerlib/BUILD b/src/python/grapl_analyzerlib/BUILD
index 5cba106bc0..2627e01edf 100644
--- a/src/python/grapl_analyzerlib/BUILD
+++ b/src/python/grapl_analyzerlib/BUILD
@@ -2,7 +2,7 @@ grapl_python_distribution(
     dependencies = [
         "./grapl_analyzerlib",
         "./grapl_analyzerlib:py_typed",
-        "./grapl_analyzerlib/nodes"
+        "./grapl_analyzerlib/nodes",
     ],
     provides = setup_py(
         name = "grapl-analyzerlib",
diff --git a/src/python/grapl_analyzerlib/dist/grapl_analyzerlib-0.2.73-py3.9.egg b/src/python/grapl_analyzerlib/dist/grapl_analyzerlib-0.2.73-py3.9.egg
new file mode 100644
index 0000000000..51373d61dc
Binary files /dev/null and b/src/python/grapl_analyzerlib/dist/grapl_analyzerlib-0.2.73-py3.9.egg differ
diff --git a/src/python/grapl_analyzerlib/grapl_analyzerlib/BUILD b/src/python/grapl_analyzerlib/grapl_analyzerlib/BUILD
index 93f8abc923..ad10c50bdf 100644
--- a/src/python/grapl_analyzerlib/grapl_analyzerlib/BUILD
+++ b/src/python/grapl_analyzerlib/grapl_analyzerlib/BUILD
@@ -1,11 +1,3 @@
-python_library(
-    dependencies=[
-        # TODO: Ideally, this would be picked up by Pants' automatic
-        # dependency resolution, but something appears to be off with
-        # it.
-        "src/proto:graplinc"
-    ],
-    skip_mypy=True
-)
+python_library()
 
 py_typed()
diff --git a/src/python/grapl_analyzerlib/pytype.cfg b/src/python/grapl_analyzerlib/pytype.cfg
deleted file mode 100644
index e6ae25b9d2..0000000000
--- a/src/python/grapl_analyzerlib/pytype.cfg
+++ /dev/null
@@ -1,59 +0,0 @@
-# NOTE: All relative paths are relative to the location of this file.
-
-[pytype]
-
-# Space-separated list of files or directories to exclude.
-exclude =
-
-# Space-separated list of files or directories to process.
-inputs = grapl_analyzerlib
-    .
-
-# Keep going past errors to analyze as many files as possible.
-keep_going = True
-
-# Run N jobs in parallel. When 'auto' is used, this will be equivalent to the
-# number of CPUs on the host system.
-jobs = 4
-
-# All pytype output goes here.
-output = .pytype
-
-# Paths to source code directories, separated by ':'.
-pythonpath = .:./grapl_analyzerlib/
-    .
-
-# Python version (major.minor) of the target code.
-python_version = 3.7
-
-# Check attribute values against their annotations. This flag is temporary and
-# will be removed once this behavior is enabled by default.
-check_attribute_types = False
-
-# Check container mutations against their annotations. This flag is temporary
-# and will be removed once this behavior is enabled by default.
-check_container_types = False
-
-# Check parameter defaults and assignments against their annotations. This flag
-# is temporary and will be removed once this behavior is enabled by default.
-check_parameter_types = False
-
-# Check variable values against their annotations. This flag is temporary and
-# will be removed once this behavior is enabled by default.
-check_variable_types = False
-
-# Comma or space separated list of error names to ignore.
-disable =
-    pyi-error
-
-# Report errors.
-report_errors = True
-
-# Experimental: Infer precise return types even for invalid function calls.
-precise_return = False
-
-# Experimental: solve unknown types to label with structural types.
-protocols = False
-
-# Experimental: Only load submodules that are explicitly imported.
-strict_import = False
diff --git a/src/python/grapl_analyzerlib/setup.py b/src/python/grapl_analyzerlib/setup.py
deleted file mode 100644
index b5f6e7a624..0000000000
--- a/src/python/grapl_analyzerlib/setup.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import os
-
-from setuptools import setup, find_packages  # type: ignore
-
-TEST_REQUIREMENTS = [
-    "pytest",
-    # simultaneously run pytest on multiple cores with `pytest -n NUMCORES`
-    "pytest-xdist",
-    "pytest-cov",
-    "hypothesis",
-]
-
-with open("README.md") as readme:
-    README = readme.read()
-
-HERE = os.path.abspath(os.path.dirname(__file__))
-
-
-def find_version():
-    with open(os.path.join(HERE, "VERSION")) as version:
-        return version.read().strip()
-
-
-__version__ = find_version()
-
-setup(
-    name="grapl_analyzerlib",
-    version=__version__,
-    description="Library for Grapl Analyzers",
-    long_description=README,
-    url="https://github.com/insanitybit/grapl_analyzerlib/",
-    author="insanitybit",
-    author_email="insanitybit@gmail.com",
-    license="MIT",
-    classifiers=[
-        "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.7",
-    ],
-    zip_safe=False,
-    packages=find_packages(),
-    package_data={
-        "grapl_analyzerlib": ["py.typed"],
-        "grapl_analyzerlib.nodes": ["nodes/py.typed"],
-        "grapl_analyzerlib.schemas": ["schemas/py.typed"],
-    },
-    include_package_data=True,
-    install_requires=[
-        "boto3",
-        "grapl_common",
-        "graplinc_grapl_api",
-        "pydgraph",
-        "typing_extensions",
-    ],
-    extras_require={
-        "test": TEST_REQUIREMENTS,
-        "typecheck": TEST_REQUIREMENTS + ["pytype"],
-    },
-)
diff --git a/src/python/graplinc-grapl-api/VERSION b/src/python/graplinc-grapl-api/VERSION
deleted file mode 100644
index 17e51c385e..0000000000
--- a/src/python/graplinc-grapl-api/VERSION
+++ /dev/null
@@ -1,1 +0,0 @@
-0.1.1
diff --git a/src/python/graplinc-grapl-api/requirements.txt b/src/python/graplinc-grapl-api/requirements.txt
deleted file mode 100644
index bde03595e6..0000000000
--- a/src/python/graplinc-grapl-api/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-protobuf
-wheel
-
diff --git a/src/python/graplinc-grapl-api/setup.py b/src/python/graplinc-grapl-api/setup.py
deleted file mode 100644
index ab39b8eb86..0000000000
--- a/src/python/graplinc-grapl-api/setup.py
+++ /dev/null
@@ -1,214 +0,0 @@
-"""Setuptools script for Grapl graph-descriptions"""
-
-import os
-import shutil
-import subprocess
-import sys
-from distutils.command.build import build as _build
-from distutils.command.clean import clean as _clean
-from distutils.spawn import find_executable
-
-from setuptools import setup
-from setuptools.command.build_py import build_py as _build_py
-from setuptools.command.develop import develop as _develop
-from setuptools.command.egg_info import egg_info as _egg_info
-from setuptools.command.install import install as _install
-from setuptools.command.sdist import sdist as _sdist
-from setuptools.command.test import test as _test
-from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
-
-# The current directory
-HERE = os.path.abspath(os.path.dirname(__file__))
-
-# The source root of all the protobuf definitions. This is where we will look to
-# resolve all protobuf imports.
-PROTO_PATH = os.path.join(HERE, "../../proto")
-
-# Only compile our protobufs within the graplinc package hierarchy. Currently
-# there are some Google packages vendored in, and we don't need to compile them,
-# only reference them.
-OUR_PROTO_PATH = os.path.join(PROTO_PATH, "graplinc")
-
-# The directory where all the generated code will be deposited. The protobuf
-# package hierarchy will be responsible for creating a "graplinc" directory
-# here.
-OUT_DIR = HERE
-
-# The path to the final generated code within OUT_DIR.
-FINAL_PACKAGE_PATH = os.path.join(OUT_DIR, "graplinc")
-
-
-def is_comment(line):
-    """check whether a line is a comment"""
-    return line.strip().startswith("#")
-
-
-def find_requirements():
-    with open(os.path.join(HERE, "requirements.txt")) as requirements:
-        return tuple(line.strip() for line in requirements if not is_comment(line))
-
-
-def find_version():
-    with open(os.path.join(HERE, "VERSION")) as version:
-        return version.read().strip()
-
-
-def find_protoc():
-    """find the protobuf compiler
-
-    returns absolute path to protoc, raises an exception if absent
-    """
-    if "PROTOC" in os.environ:
-        return os.environ["PROTOC"]
-    protoc = find_executable("protoc")
-    if protoc is None:
-        raise Exception("Could not locate protoc")
-    return protoc
-
-
-def compile_protobuf(proto_file, proto_dir, out_dir):
-    """Compile `proto_file` to a `*_pb2.py` file in `out_dir`.
-
-    `proto_dir` is used as a `--proto_path` argument to `protoc`.
-    """
-    protoc = find_protoc()
-    sys.stdout.write(f"compiling protobuf {proto_file}\n")
-    if (
-        subprocess.call(
-            [protoc, f"--proto_path={proto_dir}", f"--python_out={out_dir}", proto_file]
-        )
-        != 0
-    ):
-        raise Exception(f"Failed to compile protobuf {proto_file}")
-
-
-def find_proto_files():
-    """Find all the .proto files under our protobuf hierarchy"""
-    for (base_path, _, files) in os.walk(OUR_PROTO_PATH):
-        for file_name in files:
-            if file_name.endswith(".proto"):
-                full_path = os.path.join(base_path, file_name)
-                print(f"Found protobuf file {full_path}")
-                yield full_path
-
-
-def compile_all_protobufs():
-    """walk the proto directory tree and compile every .proto file"""
-    for proto_file in find_proto_files():
-        compile_protobuf(proto_file, PROTO_PATH, OUT_DIR)
-    # build a package structure in the OUT_DIR
-    for (base_path, _, files) in os.walk(FINAL_PACKAGE_PATH):
-        if base_path.endswith("graplinc") or base_path.endswith("graplinc/grapl"):
-            # This are going to be a namespace packages, so don't give them an __init__.py file
-            continue
-        if "__init__.py" not in files:
-            with open(os.path.join(base_path, "__init__.py"), "w") as init:
-                init.write("# generated by graplinc-grapl-api/setup.py\n")
-                init.write("import sys\n")
-                init.write("from pathlib import Path\n\n")
-                init.write("HERE = str(Path(__file__).parent)\n")
-                init.write("if HERE not in sys.path:\n")
-                init.write("    sys.path.append(HERE)\n")
-
-
-class bdist_wheel(_bdist_wheel):
-    def run(self):
-        compile_all_protobufs()
-        _bdist_wheel.run(self)
-
-
-class build(_build):
-    def run(self):
-        compile_all_protobufs()
-        _build.run(self)
-
-
-class build_py(_build_py):
-    def run(self):
-        compile_all_protobufs()
-        _build_py.run(self)
-
-
-class clean(_clean):
-    def run(self):
-        if os.path.exists(FINAL_PACKAGE_PATH):
-            shutil.rmtree(FINAL_PACKAGE_PATH)
-        build_dir = os.path.join(HERE, "build")
-        if os.path.exists(build_dir):
-            shutil.rmtree(build_dir)
-        dist_dir = os.path.join(HERE, "dist")
-        if os.path.exists(dist_dir):
-            shutil.rmtree(dist_dir)
-        egg_info_dir = os.path.join(HERE, "graplinc_grapl_api.egg-info")
-        if os.path.exists(egg_info_dir):
-            shutil.rmtree(egg_info_dir)
-        staging_dir = os.path.join(HERE, f"graplinc-grapl-api-{find_version()}")
-        if os.path.exists(staging_dir):
-            shutil.rmtree(staging_dir)
-
-        _clean.run(self)
-
-
-class develop(_develop):
-    def run(self):
-        compile_all_protobufs()
-        _develop.run(self)
-
-
-class egg_info(_egg_info):
-    def run(self):
-        compile_all_protobufs()
-        _egg_info.run(self)
-
-
-class install(_install):
-    def run(self):
-        compile_all_protobufs()
-        _install.run(self)
-
-
-class sdist(_sdist):
-    def run(self):
-        compile_all_protobufs()
-        _sdist.run(self)
-
-
-class test(_test):
-    def run(self):
-        compile_all_protobufs()
-        _test.run(self)
-
-
-__version__ = find_version()
-
-setup(
-    name="graplinc-grapl-api",
-    version=__version__,
-    author="Grapl, Inc.",
-    author_email="grapl.code@graplsecurity.com",
-    url="https://github.com/grapl-security/grapl",
-    description="Grapl API protobuf definitions",
-    # Manually listing packages for now because using find_namespace_packages
-    # here actually runs before the code is generated.
-    packages=(
-        [
-            "graplinc.grapl.api",
-            "graplinc.grapl.api.graph",
-            "graplinc.grapl.api.graph.v1beta1",
-        ]
-    ),
-    zip_safe=False,
-    cmdclass={
-        "bdist_wheel": bdist_wheel,
-        "build": build,
-        "build_py": build_py,
-        "clean": clean,
-        "develop": develop,
-        "egg_info": egg_info,
-        "install": install,
-        "sdist": sdist,
-        "test": test,
-    },
-    install_requires=find_requirements(),
-    setup_requires=("wheel",),
-)
diff --git a/src/python/python-proto/BUILD b/src/python/python-proto/BUILD
new file mode 100644
index 0000000000..a36f9e89ad
--- /dev/null
+++ b/src/python/python-proto/BUILD
@@ -0,0 +1,11 @@
+grapl_python_distribution(
+    dependencies = [
+        "./python_proto",
+    ],
+    provides = setup_py(
+        name = "python_proto",
+        version = "0.0.1",
+        readme = "README.md",
+        description = "protobuf serde library for grapl's python services",
+    ),
+)
diff --git a/src/python/python-proto/python_proto/BUILD b/src/python/python-proto/python_proto/BUILD
new file mode 100644
index 0000000000..e1b5110496
--- /dev/null
+++ b/src/python/python-proto/python_proto/BUILD
@@ -0,0 +1,5 @@
+python_library(
+    dependencies=[
+        "src/proto:graplinc"
+    ]
+)
diff --git a/src/python/python-proto/python_proto/README.md b/src/python/python-proto/python_proto/README.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/python/python-proto/python_proto/__init__.py b/src/python/python-proto/python_proto/__init__.py
new file mode 100644
index 0000000000..1f2e43afc3
--- /dev/null
+++ b/src/python/python-proto/python_proto/__init__.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod, abstractstaticmethod
+from typing import Any, cast
+
+
+class SerDe(metaclass=ABCMeta):
+    @classmethod
+    def __subclasshook__(cls, subclass: SerDe) -> bool:
+        return (
+            hasattr(subclass, "deserialize")
+            and callable(subclass.deserialize)
+            and hasattr(subclass, "serialize")
+            and callable(subclass.serialize)
+            and hasattr(subclass, "from_proto")
+            and callable(subclass.from_proto)
+            and hasattr(subclass, "into_proto")
+            and callable(subclass.into_proto)
+        )
+
+    @staticmethod
+    @abstractstaticmethod
+    def deserialize(bytes_: bytes) -> SerDe:
+        raise NotImplementedError
+
+    def serialize(self) -> bytes:
+        return cast(bytes, self.into_proto().SerializeToString())
+
+    @staticmethod
+    @abstractstaticmethod
+    def from_proto(proto: Any) -> SerDe:
+        raise NotImplementedError
+
+    @abstractmethod
+    def into_proto(self) -> Any:
+        raise NotImplementedError
diff --git a/src/python/python-proto/python_proto/api.py b/src/python/python-proto/python_proto/api.py
new file mode 100644
index 0000000000..3be5f20149
--- /dev/null
+++ b/src/python/python-proto/python_proto/api.py
@@ -0,0 +1,713 @@
+from __future__ import annotations
+
+import dataclasses
+from typing import Mapping, Sequence, Union, cast
+
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    DecrementOnlyIntProp as _DecrementOnlyIntProp,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    DecrementOnlyUintProp as _DecrementOnlyUintProp,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import Edge as _Edge
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import EdgeList as _EdgeList
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    GraphDescription as _GraphDescription,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    IdentifiedGraph as _IdentifiedGraph,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import IdentifiedNode as _IdentifiedNode
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import IdStrategy as _IdStrategy
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    ImmutableIntProp as _ImmutableIntProp,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    ImmutableStrProp as _ImmutableStrProp,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    ImmutableUintProp as _ImmutableUintProp,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    IncrementOnlyIntProp as _IncrementOnlyIntProp,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    IncrementOnlyUintProp as _IncrementOnlyUintProp,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import MergedEdge as _MergedEdge
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import MergedEdgeList as _MergedEdgeList
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import MergedGraph as _MergedGraph
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import MergedNode as _MergedNode
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import (
+    NodeDescription as _NodeDescription,
+)
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import NodeProperty as _NodeProperty
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import Session as _Session
+from graplinc.grapl.api.graph.v1beta1.types_pb2 import Static as _Static
+from python_proto import SerDe
+
+
+@dataclasses.dataclass(frozen=True)
+class Session(SerDe):
+    primary_key_properties: Sequence[str]
+    primary_key_requires_asset_id: bool
+    create_time: int
+    last_seen_time: int
+    terminate_time: int
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Session:
+        proto_session = _Session()
+        proto_session.ParseFromString(bytes_)
+        return Session.from_proto(proto_session=proto_session)
+
+    @staticmethod
+    def from_proto(proto_session: _Session) -> Session:
+        return Session(
+            primary_key_properties=proto_session.primary_key_properties,
+            primary_key_requires_asset_id=proto_session.primary_key_requires_asset_id,
+            create_time=proto_session.create_time,
+            last_seen_time=proto_session.last_seen_time,
+            terminate_time=proto_session.terminate_time,
+        )
+
+    def into_proto(self) -> _Session:
+        proto_session = _Session()
+        for prop in self.primary_key_properties:
+            proto_session.primary_key_properties.append(prop)
+        proto_session.primary_key_requires_asset_id = self.primary_key_requires_asset_id
+        proto_session.create_time = self.create_time
+        proto_session.last_seen_time = self.last_seen_time
+        proto_session.terminate_time = self.terminate_time
+        return proto_session
+
+
+@dataclasses.dataclass(frozen=True)
+class Static(SerDe):
+    primary_key_properties: Sequence[str]
+    primary_key_requires_asset_id: bool
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Static:
+        proto_static = _Static()
+        proto_static.ParseFromString(bytes_)
+        return Static.from_proto(proto_static=proto_static)
+
+    @staticmethod
+    def from_proto(proto_static: _Static) -> Static:
+        return Static(
+            primary_key_properties=proto_static.primary_key_properties,
+            primary_key_requires_asset_id=proto_static.primary_key_requires_asset_id,
+        )
+
+    def into_proto(self) -> _Static:
+        proto_static = _Static()
+        for prop in self.primary_key_properties:
+            proto_static.primary_key_properties.append(prop)
+        proto_static.primary_key_requires_asset_id = self.primary_key_requires_asset_id
+        return proto_static
+
+
+@dataclasses.dataclass(frozen=True)
+class IdStrategy(SerDe):
+    strategy: Union[Session, Static]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> IdStrategy:
+        proto_id_strategy = _IdStrategy()
+        proto_id_strategy.ParseFromString(bytes_)
+        return IdStrategy.from_proto(proto_id_strategy=proto_id_strategy)
+
+    @staticmethod
+    def from_proto(proto_id_strategy: _IdStrategy) -> IdStrategy:
+        if proto_id_strategy.HasField("session"):
+            return IdStrategy(strategy=Session.from_proto(proto_id_strategy.session))
+        elif proto_id_strategy.HasField("static"):
+            return IdStrategy(strategy=Static.from_proto(proto_id_strategy.static))
+        else:
+            raise Exception("Encountered unknown type")
+
+    def into_proto(self) -> _IdStrategy:
+        proto_id_strategy = _IdStrategy()
+        if type(self.strategy) is Session:
+            proto_id_strategy.session.CopyFrom(
+                cast(_Session, self.strategy.into_proto())
+            )
+        elif type(self.strategy) is Static:
+            proto_id_strategy.static.CopyFrom(cast(_Static, self.strategy.into_proto()))
+        else:
+            raise Exception("Encountered unknown type")
+        return proto_id_strategy
+
+
+@dataclasses.dataclass(frozen=True)
+class IncrementOnlyUintProp(SerDe):
+    prop: int
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> IncrementOnlyUintProp:
+        proto_increment_only_uint_prop = _IncrementOnlyUintProp()
+        proto_increment_only_uint_prop.ParseFromString(bytes_)
+        return IncrementOnlyUintProp.from_proto(
+            proto_increment_only_uint_prop=proto_increment_only_uint_prop
+        )
+
+    @staticmethod
+    def from_proto(
+        proto_increment_only_uint_prop: _IncrementOnlyUintProp,
+    ) -> IncrementOnlyUintProp:
+        return IncrementOnlyUintProp(prop=proto_increment_only_uint_prop.prop)
+
+    def into_proto(self) -> _IncrementOnlyUintProp:
+        proto_increment_only_uint_prop = _IncrementOnlyUintProp()
+        proto_increment_only_uint_prop.prop = self.prop
+        return proto_increment_only_uint_prop
+
+
+@dataclasses.dataclass(frozen=True)
+class ImmutableUintProp(SerDe):
+    prop: int
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> ImmutableUintProp:
+        proto_immutable_uint_prop = _ImmutableUintProp()
+        proto_immutable_uint_prop.ParseFromString(bytes_)
+        return ImmutableUintProp.from_proto(
+            proto_immutable_uint_prop=proto_immutable_uint_prop
+        )
+
+    @staticmethod
+    def from_proto(proto_immutable_uint_prop: _ImmutableUintProp) -> ImmutableUintProp:
+        return ImmutableUintProp(prop=proto_immutable_uint_prop.prop)
+
+    def into_proto(self) -> _ImmutableUintProp:
+        proto_immutable_uint_prop = _ImmutableUintProp()
+        proto_immutable_uint_prop.prop = self.prop
+        return proto_immutable_uint_prop
+
+
+@dataclasses.dataclass(frozen=True)
+class DecrementOnlyUintProp(SerDe):
+    prop: int
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> DecrementOnlyUintProp:
+        proto_decrement_only_uint_prop = _DecrementOnlyUintProp()
+        proto_decrement_only_uint_prop.ParseFromString(bytes_)
+        return DecrementOnlyUintProp.from_proto(
+            proto_decrement_only_uint_prop=proto_decrement_only_uint_prop
+        )
+
+    @staticmethod
+    def from_proto(
+        proto_decrement_only_uint_prop: _DecrementOnlyUintProp,
+    ) -> DecrementOnlyUintProp:
+        return DecrementOnlyUintProp(prop=proto_decrement_only_uint_prop.prop)
+
+    def into_proto(self) -> _DecrementOnlyUintProp:
+        proto_decrement_only_uint_prop = _DecrementOnlyUintProp()
+        proto_decrement_only_uint_prop.prop = self.prop
+        return proto_decrement_only_uint_prop
+
+
+@dataclasses.dataclass(frozen=True)
+class IncrementOnlyIntProp(SerDe):
+    prop: int
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> IncrementOnlyIntProp:
+        proto_increment_only_int_prop = _IncrementOnlyIntProp()
+        proto_increment_only_int_prop.ParseFromString(bytes_)
+        return IncrementOnlyIntProp.from_proto(
+            proto_increment_only_int_prop=proto_increment_only_int_prop
+        )
+
+    @staticmethod
+    def from_proto(
+        proto_increment_only_int_prop: _IncrementOnlyIntProp,
+    ) -> IncrementOnlyIntProp:
+        return IncrementOnlyIntProp(prop=proto_increment_only_int_prop.prop)
+
+    def into_proto(self) -> _IncrementOnlyIntProp:
+        proto_increment_only_int_prop = _IncrementOnlyIntProp()
+        proto_increment_only_int_prop.prop = self.prop
+        return proto_increment_only_int_prop
+
+
+@dataclasses.dataclass(frozen=True)
+class DecrementOnlyIntProp(SerDe):
+    prop: int
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> DecrementOnlyIntProp:
+        proto_decrement_only_int_prop = _DecrementOnlyIntProp()
+        proto_decrement_only_int_prop.ParseFromString(bytes_)
+        return DecrementOnlyIntProp.from_proto(
+            proto_decrement_only_int_prop=proto_decrement_only_int_prop
+        )
+
+    @staticmethod
+    def from_proto(
+        proto_decrement_only_int_prop: _DecrementOnlyIntProp,
+    ) -> DecrementOnlyIntProp:
+        return DecrementOnlyIntProp(prop=proto_decrement_only_int_prop.prop)
+
+    def into_proto(self) -> _DecrementOnlyIntProp:
+        proto_decrement_only_int_prop = _DecrementOnlyIntProp()
+        proto_decrement_only_int_prop.prop = self.prop
+        return proto_decrement_only_int_prop
+
+
+@dataclasses.dataclass(frozen=True)
+class ImmutableIntProp(SerDe):
+    prop: int
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> ImmutableIntProp:
+        proto_immutable_int_prop = _ImmutableIntProp()
+        proto_immutable_int_prop.ParseFromString(bytes_)
+        return ImmutableIntProp.from_proto(
+            proto_immutable_int_prop=proto_immutable_int_prop
+        )
+
+    @staticmethod
+    def from_proto(proto_immutable_int_prop: _ImmutableIntProp) -> ImmutableIntProp:
+        return ImmutableIntProp(prop=proto_immutable_int_prop.prop)
+
+    def into_proto(self) -> _ImmutableIntProp:
+        proto_immutable_int_prop = _ImmutableIntProp()
+        proto_immutable_int_prop.prop = self.prop
+        return proto_immutable_int_prop
+
+
+@dataclasses.dataclass(frozen=True)
+class ImmutableStrProp(SerDe):
+    prop: str
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> ImmutableStrProp:
+        proto_immutable_str_prop = _ImmutableStrProp()
+        proto_immutable_str_prop.ParseFromString(bytes_)
+        return ImmutableStrProp.from_proto(
+            proto_immutable_str_prop=proto_immutable_str_prop
+        )
+
+    @staticmethod
+    def from_proto(proto_immutable_str_prop: _ImmutableStrProp) -> ImmutableStrProp:
+        return ImmutableStrProp(prop=proto_immutable_str_prop.prop)
+
+    def into_proto(self) -> _ImmutableStrProp:
+        proto_immutable_str_prop = _ImmutableStrProp()
+        proto_immutable_str_prop.prop = self.prop
+        return proto_immutable_str_prop
+
+
+@dataclasses.dataclass(frozen=True)
+class NodeProperty(SerDe):
+    property_: Union[
+        IncrementOnlyUintProp,
+        DecrementOnlyUintProp,
+        ImmutableUintProp,
+        IncrementOnlyIntProp,
+        DecrementOnlyIntProp,
+        ImmutableIntProp,
+        ImmutableStrProp,
+    ]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> NodeProperty:
+        proto_node_property = _NodeProperty()
+        proto_node_property.ParseFromString(bytes_)
+        return NodeProperty.from_proto(proto_node_property=proto_node_property)
+
+    @staticmethod
+    def from_proto(proto_node_property: _NodeProperty) -> NodeProperty:
+        if proto_node_property.HasField("increment_only_uint"):
+            return NodeProperty(
+                property_=IncrementOnlyUintProp.from_proto(
+                    proto_node_property.increment_only_uint
+                )
+            )
+        elif proto_node_property.HasField("decrement_only_uint"):
+            return NodeProperty(
+                property_=DecrementOnlyUintProp.from_proto(
+                    proto_node_property.decrement_only_uint
+                )
+            )
+        elif proto_node_property.HasField("immutable_uint"):
+            return NodeProperty(
+                property_=ImmutableUintProp.from_proto(
+                    proto_node_property.immutable_uint
+                )
+            )
+        elif proto_node_property.HasField("increment_only_int"):
+            return NodeProperty(
+                property_=IncrementOnlyIntProp.from_proto(
+                    proto_node_property.increment_only_int
+                )
+            )
+        elif proto_node_property.HasField("decrement_only_int"):
+            return NodeProperty(
+                property_=DecrementOnlyIntProp.from_proto(
+                    proto_node_property.decrement_only_int
+                )
+            )
+        elif proto_node_property.HasField("immutable_int"):
+            return NodeProperty(
+                property_=ImmutableIntProp.from_proto(proto_node_property.immutable_int)
+            )
+        elif proto_node_property.HasField("immutable_str"):
+            return NodeProperty(
+                property_=ImmutableStrProp.from_proto(proto_node_property.immutable_str)
+            )
+        else:
+            raise Exception("Encountered unknown type")
+
+    def into_proto(self) -> _NodeProperty:
+        proto_node_property = _NodeProperty()
+        if type(self.property_) is IncrementOnlyUintProp:
+            proto_node_property.increment_only_uint.CopyFrom(
+                cast(_IncrementOnlyUintProp, self.property_.into_proto())
+            )
+        elif type(self.property_) is DecrementOnlyUintProp:
+            proto_node_property.decrement_only_uint.CopyFrom(
+                cast(_DecrementOnlyUintProp, self.property_.into_proto())
+            )
+        elif type(self.property_) is ImmutableUintProp:
+            proto_node_property.immutable_uint.CopyFrom(
+                cast(_ImmutableUintProp, self.property_.into_proto())
+            )
+        elif type(self.property_) is IncrementOnlyIntProp:
+            proto_node_property.increment_only_int.CopyFrom(
+                cast(_IncrementOnlyIntProp, self.property_.into_proto())
+            )
+        elif type(self.property_) is DecrementOnlyIntProp:
+            proto_node_property.decrement_only_int.CopyFrom(
+                cast(_DecrementOnlyIntProp, self.property_.into_proto())
+            )
+        elif type(self.property_) is ImmutableIntProp:
+            proto_node_property.immutable_int.CopyFrom(
+                cast(_ImmutableIntProp, self.property_.into_proto())
+            )
+        elif type(self.property_) is ImmutableStrProp:
+            proto_node_property.immutable_str.CopyFrom(
+                cast(_ImmutableStrProp, self.property_.into_proto())
+            )
+        else:
+            raise Exception("Encountered unknown type")
+        return proto_node_property
+
+
+@dataclasses.dataclass(frozen=True)
+class NodeDescription(SerDe):
+    properties: Mapping[str, NodeProperty]
+    node_key: str
+    node_type: str
+    id_strategy: Sequence[IdStrategy]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> NodeDescription:
+        proto_node_description = _NodeDescription()
+        proto_node_description.ParseFromString(bytes_)
+        return NodeDescription.from_proto(proto_node_description=proto_node_description)
+
+    @staticmethod
+    def from_proto(proto_node_description: _NodeDescription) -> NodeDescription:
+        return NodeDescription(
+            properties={
+                k: NodeProperty.from_proto(proto_node_description.properties[k])
+                for k in proto_node_description.properties
+            },
+            node_key=proto_node_description.node_key,
+            node_type=proto_node_description.node_type,
+            id_strategy=[
+                IdStrategy.from_proto(s) for s in proto_node_description.id_strategy
+            ],
+        )
+
+    def into_proto(self) -> _NodeDescription:
+        proto_node_description = _NodeDescription()
+        for k, v in self.properties.items():
+            proto_node_description.properties[k].CopyFrom(v.into_proto())
+        proto_node_description.node_key = self.node_key
+        proto_node_description.node_type = self.node_type
+        for s in self.id_strategy:
+            proto_node_description.id_strategy.append(s.into_proto())
+        return proto_node_description
+
+
+@dataclasses.dataclass(frozen=True)
+class IdentifiedNode(SerDe):
+    properties: Mapping[str, NodeProperty]
+    node_key: str
+    node_type: str
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> IdentifiedNode:
+        proto_identified_node = _IdentifiedNode()
+        proto_identified_node.ParseFromString(bytes_)
+        return IdentifiedNode.from_proto(proto_identified_node=proto_identified_node)
+
+    @staticmethod
+    def from_proto(proto_identified_node: _IdentifiedNode) -> IdentifiedNode:
+        return IdentifiedNode(
+            properties={
+                k: NodeProperty.from_proto(proto_identified_node.properties[k])
+                for k in proto_identified_node.properties
+            },
+            node_key=proto_identified_node.node_key,
+            node_type=proto_identified_node.node_type,
+        )
+
+    def into_proto(self) -> _IdentifiedNode:
+        proto_identified_node = _IdentifiedNode()
+        for k, v in self.properties.items():
+            proto_identified_node.properties[k].CopyFrom(v.into_proto())
+        proto_identified_node.node_key = self.node_key
+        proto_identified_node.node_type = self.node_type
+        return proto_identified_node
+
+
+@dataclasses.dataclass(frozen=True)
+class MergedNode(SerDe):
+    properties: Mapping[str, NodeProperty]
+    uid: int
+    node_key: str
+    node_type: str
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> MergedNode:
+        proto_merged_node = _MergedNode()
+        proto_merged_node.ParseFromString(bytes_)
+        return MergedNode.from_proto(proto_merged_node=proto_merged_node)
+
+    @staticmethod
+    def from_proto(proto_merged_node: _MergedNode) -> MergedNode:
+        return MergedNode(
+            properties={
+                k: NodeProperty.from_proto(proto_merged_node.properties[k])
+                for k in proto_merged_node.properties
+            },
+            uid=proto_merged_node.uid,
+            node_key=proto_merged_node.node_key,
+            node_type=proto_merged_node.node_type,
+        )
+
+    def into_proto(self) -> _MergedNode:
+        proto_merged_node = _MergedNode()
+        for k, v in self.properties.items():
+            proto_merged_node.properties[k].CopyFrom(v.into_proto())
+        proto_merged_node.uid = self.uid
+        proto_merged_node.node_key = self.node_key
+        proto_merged_node.node_type = self.node_type
+        return proto_merged_node
+
+
+@dataclasses.dataclass(frozen=True)
+class Edge(SerDe):
+    from_node_key: str
+    to_node_key: str
+    edge_name: str
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Edge:
+        proto_edge = _Edge()
+        proto_edge.ParseFromString(bytes_)
+        return Edge.from_proto(proto_edge=proto_edge)
+
+    @staticmethod
+    def from_proto(proto_edge: _Edge) -> Edge:
+        return Edge(
+            from_node_key=proto_edge.from_node_key,
+            to_node_key=proto_edge.to_node_key,
+            edge_name=proto_edge.edge_name,
+        )
+
+    def into_proto(self) -> _Edge:
+        proto_edge = _Edge()
+        proto_edge.from_node_key = self.from_node_key
+        proto_edge.to_node_key = self.to_node_key
+        proto_edge.edge_name = self.edge_name
+        return proto_edge
+
+
+@dataclasses.dataclass(frozen=True)
+class EdgeList(SerDe):
+    edges: Sequence[Edge]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> EdgeList:
+        proto_edge_list = _EdgeList()
+        proto_edge_list.ParseFromString(bytes_)
+        return EdgeList.from_proto(proto_edge_list=proto_edge_list)
+
+    @staticmethod
+    def from_proto(proto_edge_list: _EdgeList) -> EdgeList:
+        return EdgeList(edges=[Edge.from_proto(e) for e in proto_edge_list.edges])
+
+    def into_proto(self) -> _EdgeList:
+        proto_edge_list = _EdgeList()
+        for e in self.edges:
+            proto_edge_list.edges.append(e.into_proto())
+        return proto_edge_list
+
+
+@dataclasses.dataclass(frozen=True)
+class MergedEdge(SerDe):
+    from_uid: str
+    from_node_key: str
+    to_uid: str
+    to_node_key: str
+    edge_name: str
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> MergedEdge:
+        proto_merged_edge = _MergedEdge()
+        proto_merged_edge.ParseFromString(bytes_)
+        return MergedEdge.from_proto(proto_merged_edge=proto_merged_edge)
+
+    @staticmethod
+    def from_proto(proto_merged_edge: _MergedEdge) -> MergedEdge:
+        return MergedEdge(
+            from_uid=proto_merged_edge.from_uid,
+            from_node_key=proto_merged_edge.from_node_key,
+            to_uid=proto_merged_edge.to_uid,
+            to_node_key=proto_merged_edge.to_node_key,
+            edge_name=proto_merged_edge.edge_name,
+        )
+
+    def into_proto(self) -> _MergedEdge:
+        proto_merged_edge = _MergedEdge()
+        proto_merged_edge.from_uid = self.from_uid
+        proto_merged_edge.from_node_key = self.from_node_key
+        proto_merged_edge.to_uid = self.to_uid
+        proto_merged_edge.to_node_key = self.to_node_key
+        proto_merged_edge.edge_name = self.edge_name
+        return proto_merged_edge
+
+
+@dataclasses.dataclass(frozen=True)
+class MergedEdgeList(SerDe):
+    # TODO: seed to places where this is used:
+    # /src/python/grapl_analyzerlib/grapl_analyzerlib/view_from_proto.py
+    # /src/python/grapl_analyzerlib/grapl_analyzerlib/subgraph_view.py
+    edges: Sequence[MergedEdge]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> MergedEdgeList:
+        proto_merged_edge_list = _MergedEdgeList()
+        proto_merged_edge_list.ParseFromString(bytes_)
+        return MergedEdgeList.from_proto(proto_merged_edge_list=proto_merged_edge_list)
+
+    @staticmethod
+    def from_proto(proto_merged_edge_list: _MergedEdgeList) -> MergedEdgeList:
+        return MergedEdgeList(
+            edges=[MergedEdge.from_proto(e) for e in proto_merged_edge_list.edges]
+        )
+
+    def into_proto(self) -> _MergedEdgeList:
+        proto_merged_edge_list = _MergedEdgeList()
+        for e in self.edges:
+            proto_merged_edge_list.edges.append(e.into_proto())
+        return proto_merged_edge_list
+
+
+@dataclasses.dataclass(frozen=True)
+class GraphDescription(SerDe):
+    nodes: Mapping[str, NodeDescription]
+    edges: Mapping[str, EdgeList]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> GraphDescription:
+        proto_graph_description = _GraphDescription()
+        proto_graph_description.ParseFromString(bytes_)
+        return GraphDescription.from_proto(
+            proto_graph_description=proto_graph_description
+        )
+
+    @staticmethod
+    def from_proto(proto_graph_description: _GraphDescription) -> GraphDescription:
+        return GraphDescription(
+            nodes={
+                k: NodeDescription.from_proto(proto_graph_description.nodes[k])
+                for k in proto_graph_description.nodes
+            },
+            edges={
+                k: EdgeList.from_proto(proto_graph_description.edges[k])
+                for k in proto_graph_description.edges
+            },
+        )
+
+    def into_proto(self) -> _GraphDescription:
+        proto_graph_description = _GraphDescription()
+        for k1, v1 in self.nodes.items():
+            proto_graph_description.nodes[k1].CopyFrom(v1.into_proto())
+        for k2, v2 in self.edges.items():
+            proto_graph_description.edges[k2].CopyFrom(v2.into_proto())
+        return proto_graph_description
+
+
+@dataclasses.dataclass(frozen=True)
+class IdentifiedGraph(SerDe):
+    nodes: Mapping[str, IdentifiedNode]
+    edges: Mapping[str, EdgeList]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> IdentifiedGraph:
+        proto_identified_graph = _IdentifiedGraph()
+        proto_identified_graph.ParseFromString(bytes_)
+        return IdentifiedGraph.from_proto(proto_identified_graph=proto_identified_graph)
+
+    @staticmethod
+    def from_proto(proto_identified_graph: _IdentifiedGraph) -> IdentifiedGraph:
+        return IdentifiedGraph(
+            nodes={
+                k: IdentifiedNode.from_proto(proto_identified_graph.nodes[k])
+                for k in proto_identified_graph.nodes
+            },
+            edges={
+                k: EdgeList.from_proto(proto_identified_graph.edges[k])
+                for k in proto_identified_graph.edges
+            },
+        )
+
+    def into_proto(self) -> _IdentifiedGraph:
+        proto_identified_graph = _IdentifiedGraph()
+        for k1, v1 in self.nodes.items():
+            proto_identified_graph.nodes[k1].CopyFrom(v1.into_proto())
+        for k2, v2 in self.edges.items():
+            proto_identified_graph.edges[k2].CopyFrom(v2.into_proto())
+        return proto_identified_graph
+
+
+@dataclasses.dataclass(frozen=True)
+class MergedGraph(SerDe):
+    nodes: Mapping[str, MergedNode]
+    edges: Mapping[str, MergedEdgeList]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> MergedGraph:
+        proto_merged_graph = _MergedGraph()
+        proto_merged_graph.ParseFromString(bytes_)
+        return MergedGraph.from_proto(proto_merged_graph=proto_merged_graph)
+
+    @staticmethod
+    def from_proto(proto_merged_graph: _MergedGraph) -> MergedGraph:
+        return MergedGraph(
+            nodes={
+                k: MergedNode.from_proto(proto_merged_graph.nodes[k])
+                for k in proto_merged_graph.nodes
+            },
+            edges={
+                k: MergedEdgeList.from_proto(proto_merged_graph.edges[k])
+                for k in proto_merged_graph.edges
+            },
+        )
+
+    def into_proto(self) -> _MergedGraph:
+        proto_merged_graph = _MergedGraph()
+        for k1, v1 in self.nodes.items():
+            proto_merged_graph.nodes[k1].CopyFrom(v1.into_proto())
+        for k2, v2 in self.edges.items():
+            proto_merged_graph.edges[k2].CopyFrom(v2.into_proto())
+        return proto_merged_graph
diff --git a/src/python/python-proto/python_proto/metrics.py b/src/python/python-proto/python_proto/metrics.py
new file mode 100644
index 0000000000..eae2c0eb15
--- /dev/null
+++ b/src/python/python-proto/python_proto/metrics.py
@@ -0,0 +1,177 @@
+from __future__ import annotations
+
+import dataclasses
+import enum
+from typing import Sequence, Union, cast
+
+from graplinc.grapl.metrics.v1.metric_types_pb2 import Counter as _Counter
+from graplinc.grapl.metrics.v1.metric_types_pb2 import Gauge as _Gauge
+from graplinc.grapl.metrics.v1.metric_types_pb2 import Histogram as _Histogram
+from graplinc.grapl.metrics.v1.metric_types_pb2 import Label as _Label
+from graplinc.grapl.metrics.v1.metric_types_pb2 import MetricWrapper as _MetricWrapper
+from python_proto import SerDe
+
+
+@dataclasses.dataclass(frozen=True)
+class Label(SerDe):
+    key: str
+    value: str
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Label:
+        proto_label = _Label()
+        proto_label.ParseFromString(bytes_)
+        return Label.from_proto(proto_label=proto_label)
+
+    @staticmethod
+    def from_proto(proto_label: _Label) -> Label:
+        return Label(key=proto_label.key, value=proto_label.value)
+
+    def into_proto(self) -> _Label:
+        proto_label = _Label()
+        proto_label.key = self.key
+        proto_label.value = self.value
+        return proto_label
+
+
+@dataclasses.dataclass(frozen=True)
+class Counter(SerDe):
+    name: str
+    increment: int
+    labels: Sequence[Label]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Counter:
+        proto_counter = _Counter()
+        proto_counter.ParseFromString(bytes_)
+        return Counter.from_proto(proto_counter=proto_counter)
+
+    @staticmethod
+    def from_proto(proto_counter: _Counter) -> Counter:
+        return Counter(
+            name=proto_counter.name,
+            increment=proto_counter.increment,
+            labels=[Label.from_proto(l) for l in proto_counter.labels],
+        )
+
+    def into_proto(self) -> _Counter:
+        proto_counter = _Counter()
+        proto_counter.name = self.name
+        proto_counter.increment = self.increment
+        for label in self.labels:
+            proto_counter.labels.append(label.into_proto())
+        return proto_counter
+
+
+class GaugeType(enum.Enum):
+    GAUGE_TYPE_UNSPECIFIED = "GAUGE_TYPE_UNSPECIFIED"
+    GAUGE_TYPE_ABSOLUTE = "GAUGE_TYPE_ABSOLUTE"
+    GAUGE_TYPE_INCREMENT = "GAUGE_TYPE_INCREMENT"
+    GAUGE_TYPE_DECREMENT = "GAUGE_TYPE_DECREMENT"
+
+
+@dataclasses.dataclass(frozen=True)
+class Gauge(SerDe):
+    gauge_type: GaugeType
+    name: str
+    value: float
+    labels: Sequence[Label]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Gauge:
+        proto_gauge = _Gauge()
+        proto_gauge.ParseFromString(bytes_)
+        return Gauge.from_proto(proto_gauge=proto_gauge)
+
+    @staticmethod
+    def from_proto(proto_gauge: _Gauge) -> Gauge:
+        return Gauge(
+            gauge_type=GaugeType(_Gauge.GaugeType.Name(proto_gauge.gauge_type)),
+            name=proto_gauge.name,
+            value=proto_gauge.value,
+            labels=[Label.from_proto(l) for l in proto_gauge.labels],
+        )
+
+    def into_proto(self) -> _Gauge:
+        proto_gauge = _Gauge()
+        proto_gauge.gauge_type = _Gauge.GaugeType.Value(self.gauge_type.value)
+        proto_gauge.name = self.name
+        proto_gauge.value = self.value
+        for label in self.labels:
+            proto_gauge.labels.append(label.into_proto())
+        return proto_gauge
+
+
+@dataclasses.dataclass(frozen=True)
+class Histogram(SerDe):
+    name: str
+    value: float
+    labels: Sequence[Label]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Histogram:
+        proto_histogram = _Histogram()
+        proto_histogram.ParseFromString(bytes_)
+        return Histogram.from_proto(proto_histogram=proto_histogram)
+
+    @staticmethod
+    def from_proto(proto_histogram: _Histogram) -> Histogram:
+        return Histogram(
+            name=proto_histogram.name,
+            value=proto_histogram.value,
+            labels=[Label.from_proto(l) for l in proto_histogram.labels],
+        )
+
+    def into_proto(self) -> _Histogram:
+        proto_histogram = _Histogram()
+        proto_histogram.name = self.name
+        proto_histogram.value = self.value
+        for label in self.labels:
+            proto_histogram.labels.append(label.into_proto())
+        return proto_histogram
+
+
+@dataclasses.dataclass(frozen=True)
+class MetricWrapper(SerDe):
+    metric: Union[Counter, Gauge, Histogram]
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> MetricWrapper:
+        proto_metric_wrapper = _MetricWrapper()
+        proto_metric_wrapper.ParseFromString(bytes_)
+        return MetricWrapper.from_proto(proto_metric_wrapper=proto_metric_wrapper)
+
+    @staticmethod
+    def from_proto(proto_metric_wrapper: _MetricWrapper) -> MetricWrapper:
+        if proto_metric_wrapper.HasField("counter"):
+            metric = cast(
+                Union[Counter, Gauge, Histogram],
+                Counter.from_proto(proto_counter=proto_metric_wrapper.counter),
+            )
+        elif proto_metric_wrapper.HasField("gauge"):
+            metric = cast(
+                Union[Counter, Gauge, Histogram],
+                Gauge.from_proto(proto_gauge=proto_metric_wrapper.gauge),
+            )
+        elif proto_metric_wrapper.HasField("histogram"):
+            metric = cast(
+                Union[Counter, Gauge, Histogram],
+                Histogram.from_proto(proto_histogram=proto_metric_wrapper.histogram),
+            )
+        else:
+            raise Exception("Encountered unknown type")
+        return MetricWrapper(metric=metric)
+
+    def into_proto(self) -> _MetricWrapper:
+        proto_metric_wrapper = _MetricWrapper()
+        if type(self.metric) is Counter:
+            proto_metric_wrapper.counter.CopyFrom(
+                cast(_Counter, self.metric.into_proto())
+            )
+        elif type(self.metric) is Gauge:
+            proto_metric_wrapper.gauge.CopyFrom(cast(_Gauge, self.metric.into_proto()))
+        elif type(self.metric) is Histogram:
+            proto_metric_wrapper.histogram.CopyFrom(
+                cast(_Histogram, self.metric.into_proto())
+            )
+        return proto_metric_wrapper
diff --git a/src/python/python-proto/python_proto/pipeline.py b/src/python/python-proto/python_proto/pipeline.py
new file mode 100644
index 0000000000..83d2fb1db8
--- /dev/null
+++ b/src/python/python-proto/python_proto/pipeline.py
@@ -0,0 +1,111 @@
+from __future__ import annotations
+
+import dataclasses
+import uuid
+
+from graplinc.grapl.pipeline.v1beta1.types_pb2 import Envelope as _Envelope
+from graplinc.grapl.pipeline.v1beta1.types_pb2 import Metadata as _Metadata
+from graplinc.grapl.pipeline.v1beta1.types_pb2 import Uuid as _Uuid
+from python_proto import SerDe
+
+
+@dataclasses.dataclass(frozen=True)
+class Uuid(SerDe):
+    lsb: int
+    msb: int
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Uuid:
+        proto_uuid = _Uuid()
+        proto_uuid.ParseFromString(bytes_)
+        return Uuid.from_proto(proto_uuid=proto_uuid)
+
+    @staticmethod
+    def from_uuid(uuid_: uuid.UUID) -> Uuid:
+        lsb_bytes = uuid_.bytes[:8]
+        msb_bytes = uuid_.bytes[8:]
+        lsb = int.from_bytes(lsb_bytes, byteorder="little", signed=False)
+        msb = int.from_bytes(msb_bytes, byteorder="little", signed=False)
+        return Uuid(lsb=lsb, msb=msb)
+
+    def into_uuid(self) -> uuid.UUID:
+        lsb_bytes = int.to_bytes(self.lsb, byteorder="little", length=8, signed=False)
+        msb_bytes = int.to_bytes(self.msb, byteorder="little", length=8, signed=False)
+        return uuid.UUID(bytes=lsb_bytes + msb_bytes)
+
+    @staticmethod
+    def from_proto(proto_uuid: _Uuid) -> Uuid:
+        return Uuid(lsb=proto_uuid.lsb, msb=proto_uuid.msb)
+
+    def into_proto(self) -> _Uuid:
+        proto_uuid = _Uuid()
+        proto_uuid.lsb = self.lsb
+        proto_uuid.msb = self.msb
+        return proto_uuid
+
+
+@dataclasses.dataclass(frozen=True)
+class Metadata(SerDe):
+    trace_id: uuid.UUID
+    tenant_id: uuid.UUID
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Metadata:
+        proto_metadata = _Metadata()
+        proto_metadata.ParseFromString(bytes_)
+        return Metadata.from_proto(proto_metadata=proto_metadata)
+
+    @staticmethod
+    def _from_proto_parts(trace_id: _Uuid, tenant_id: _Uuid) -> Metadata:
+        return Metadata(
+            trace_id=Uuid.from_proto(trace_id).into_uuid(),
+            tenant_id=Uuid.from_proto(tenant_id).into_uuid(),
+        )
+
+    @staticmethod
+    def from_proto(proto_metadata: _Metadata) -> Metadata:
+        return Metadata._from_proto_parts(
+            proto_metadata.trace_id, proto_metadata.tenant_id
+        )
+
+    def into_proto(self) -> _Metadata:
+        proto_metadata = _Metadata()
+        proto_metadata.trace_id.CopyFrom(Uuid.from_uuid(self.trace_id).into_proto())
+        proto_metadata.tenant_id.CopyFrom(Uuid.from_uuid(self.tenant_id).into_proto())
+        return proto_metadata
+
+
+@dataclasses.dataclass(frozen=True)
+class Envelope(SerDe):
+    metadata: Metadata
+    inner_message: bytes
+    inner_type: str
+
+    @staticmethod
+    def deserialize(bytes_: bytes) -> Envelope:
+        proto_envelope = _Envelope()
+        proto_envelope.ParseFromString(bytes_)
+        return Envelope.from_proto(proto_envelope=proto_envelope)
+
+    @staticmethod
+    def from_proto(proto_envelope: _Envelope) -> Envelope:
+        return Envelope._from_proto_parts(
+            proto_envelope.metadata,
+            proto_envelope.inner_message,
+            proto_envelope.inner_type,
+        )
+
+    @staticmethod
+    def _from_proto_parts(
+        metadata: _Metadata,
+        inner_message: bytes,
+        inner_type: str,
+    ) -> Envelope:
+        return Envelope(Metadata.from_proto(metadata), inner_message, inner_type)
+
+    def into_proto(self) -> _Envelope:
+        proto_envelope = _Envelope()
+        proto_envelope.metadata.CopyFrom(self.metadata.into_proto())
+        proto_envelope.inner_message = self.inner_message
+        proto_envelope.inner_type = self.inner_type
+        return proto_envelope
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index 02edd58559..0508b62933 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -31,7 +31,7 @@ dependencies = [
  "derive_more",
  "either",
  "futures-util",
- "http 0.2.4",
+ "http 0.2.5",
  "log",
  "trust-dns-proto",
  "trust-dns-resolver",
@@ -84,7 +84,7 @@ dependencies = [
  "futures-util",
  "fxhash",
  "h2 0.2.7",
- "http 0.2.4",
+ "http 0.2.5",
  "httparse",
  "indexmap",
  "itoa",
@@ -111,7 +111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655"
 dependencies = [
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -121,7 +121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2ad299af73649e1fc893e333ccf86f377751eb95ff875d095131574c6f43452c"
 dependencies = [
  "bytestring",
- "http 0.2.4",
+ "http 0.2.5",
  "log",
  "regex",
  "serde",
@@ -212,7 +212,7 @@ dependencies = [
  "lazy_static",
  "log",
  "num_cpus",
- "parking_lot 0.11.1",
+ "parking_lot 0.11.2",
  "threadpool",
 ]
 
@@ -293,9 +293,9 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -399,12 +399,11 @@ version = "1.0.0"
 dependencies = [
  "async-trait",
  "base64 0.13.0",
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "chrono",
  "failure",
- "futures 0.3.16",
+ "futures 0.3.17",
  "grapl-config",
- "grapl-graph-descriptions",
  "grapl-observe",
  "grapl-service",
  "log",
@@ -413,11 +412,12 @@ dependencies = [
  "rusoto_credential",
  "rusoto_s3",
  "rusoto_sqs",
+ "rust-proto",
  "serde",
  "serde_json",
  "sqs-executor",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "uuid",
  "zstd",
 ]
@@ -442,21 +442,21 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.43"
+version = "1.0.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
+checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
 
 [[package]]
 name = "arc-swap"
-version = "1.3.0"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820"
+checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63"
 
 [[package]]
 name = "argon2"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e99a67e1c7e8bb14bdb2d8f4046da246e138c99f605499a0bb7aa39e9ec52435"
+checksum = "ca5162d1b961cb589a8ca08a2aa7cabc6341e05e0bf18d66a07697900b5d2ad0"
 dependencies = [
  "blake2",
  "password-hash",
@@ -494,9 +494,9 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -505,9 +505,9 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -516,9 +516,9 @@ version = "0.1.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -580,7 +580,7 @@ dependencies = [
  "base64 0.12.3",
  "bytes 0.5.6",
  "chrono",
- "http 0.2.4",
+ "http 0.2.5",
  "http-serde",
  "serde",
  "serde_derive",
@@ -622,21 +622,21 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
 [[package]]
 name = "base64ct"
-version = "1.0.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b"
+checksum = "40a96587c05c810ddbb79e2674d519cff1379517e7b91d166dff7a7cc0e9af6e"
 
 [[package]]
 name = "bitflags"
-version = "1.3.1"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2da1976d75adbe5fbc88130ecd119529cf1cc6a93ae1546d8696ee66f0d21af1"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "blake2"
-version = "0.9.1"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10a5720225ef5daecf08657f23791354e1685a8c91a4c60c7f3d3b2892f978f4"
+checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174"
 dependencies = [
  "crypto-mac 0.8.0",
  "digest",
@@ -674,9 +674,9 @@ dependencies = [
 
 [[package]]
 name = "bstr"
-version = "0.2.16"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
 dependencies = [
  "lazy_static",
  "memchr",
@@ -695,9 +695,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.7.0"
+version = "3.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
+checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"
 
 [[package]]
 name = "byteorder"
@@ -727,9 +727,9 @@ dependencies = [
 
 [[package]]
 name = "bytes"
-version = "1.0.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
 
 [[package]]
 name = "bytestring"
@@ -737,7 +737,7 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
 ]
 
 [[package]]
@@ -751,9 +751,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.69"
+version = "1.0.70"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
+checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
 dependencies = [
  "jobserver",
 ]
@@ -859,15 +859,16 @@ dependencies = [
 
 [[package]]
 name = "combine"
-version = "4.6.0"
+version = "4.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2d47c1b11006b87e492b53b313bb699ce60e16613c4dddaa91f8f7c220ab2fa"
+checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001"
 dependencies = [
- "bytes 1.0.1",
- "futures-util",
+ "bytes 1.1.0",
+ "futures-core",
  "memchr",
  "pin-project-lite 0.2.7",
- "tokio 1.10.0",
+ "tokio 1.12.0",
+ "tokio-util 0.6.8",
 ]
 
 [[package]]
@@ -923,9 +924,9 @@ checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
 
 [[package]]
 name = "cpufeatures"
-version = "0.1.5"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
+checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
 dependencies = [
  "libc",
 ]
@@ -956,7 +957,7 @@ dependencies = [
  "clap",
  "criterion-plot",
  "csv",
- "futures 0.3.16",
+ "futures 0.3.17",
  "itertools 0.10.1",
  "lazy_static",
  "num-traits",
@@ -969,7 +970,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "tinytemplate",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "walkdir",
 ]
 
@@ -1152,12 +1153,12 @@ dependencies = [
 
 [[package]]
 name = "ctor"
-version = "0.1.20"
+version = "0.1.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
+checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa"
 dependencies = [
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -1187,10 +1188,10 @@ checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
 dependencies = [
  "fnv",
  "ident_case",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
  "strsim 0.9.3",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -1201,7 +1202,7 @@ checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
 dependencies = [
  "darling_core",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -1210,23 +1211,23 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
 name = "derive-dynamic-node"
 version = "1.0.1"
 dependencies = [
- "grapl-graph-descriptions",
  "log",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
+ "rust-proto",
  "serde",
  "serde_derive",
  "serde_json",
- "syn 1.0.74",
+ "syn 1.0.76",
  "uuid",
 ]
 
@@ -1238,9 +1239,9 @@ checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
 dependencies = [
  "darling",
  "derive_builder_core",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -1250,9 +1251,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
 dependencies = [
  "darling",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -1273,10 +1274,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df"
 dependencies = [
  "convert_case",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
  "rustc_version 0.3.3",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -1288,8 +1289,8 @@ dependencies = [
  "anyhow",
  "async-stream 0.2.1",
  "async-trait",
- "futures 0.3.16",
- "http 0.2.4",
+ "futures 0.3.17",
+ "http 0.2.5",
  "lazy_static",
  "prost",
  "rand 0.7.3",
@@ -1297,7 +1298,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tonic",
  "tonic-build",
  "tracing",
@@ -1380,7 +1381,7 @@ name = "endpoint-plugin"
 version = "0.1.0"
 dependencies = [
  "derive-dynamic-node",
- "grapl-graph-descriptions",
+ "rust-proto",
  "thiserror",
  "uuid",
 ]
@@ -1392,9 +1393,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
 dependencies = [
  "heck",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -1433,9 +1434,9 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
  "synstructure 0.12.5",
 ]
 
@@ -1447,9 +1448,9 @@ checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
 
 [[package]]
 name = "flate2"
-version = "1.0.20"
+version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
+checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
 dependencies = [
  "cfg-if 1.0.0",
  "crc32fast",
@@ -1497,9 +1498,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
 
 [[package]]
 name = "futures"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b"
+checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -1512,9 +1513,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9"
+checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -1522,9 +1523,9 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
+checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
 
 [[package]]
 name = "futures-cpupool"
@@ -1538,9 +1539,9 @@ dependencies = [
 
 [[package]]
 name = "futures-executor"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c"
+checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -1549,21 +1550,21 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
+checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
 
 [[package]]
 name = "futures-macro"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57"
+checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
 dependencies = [
  "autocfg",
  "proc-macro-hack",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -1572,28 +1573,28 @@ version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fde5a672a61f96552aa5ed9fd9c81c3fbdae4be9b1e205d6eaf17c83705adc0f"
 dependencies = [
- "futures 0.3.16",
+ "futures 0.3.17",
  "pin-project-lite 0.2.7",
- "tokio 1.10.0",
+ "tokio 1.12.0",
 ]
 
 [[package]]
 name = "futures-sink"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
+checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
 
 [[package]]
 name = "futures-task"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2"
+checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
 
 [[package]]
 name = "futures-util"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
+checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
 dependencies = [
  "autocfg",
  "futures 0.1.31",
@@ -1637,9 +1638,8 @@ dependencies = [
  "async-trait",
  "endpoint-plugin",
  "eyre",
- "futures 0.3.16",
+ "futures 0.3.17",
  "grapl-config",
- "grapl-graph-descriptions",
  "grapl-observe",
  "grapl-service",
  "lazy_static",
@@ -1649,11 +1649,12 @@ dependencies = [
  "rusoto_core",
  "rusoto_s3",
  "rusoto_sqs",
+ "rust-proto",
  "serde",
  "serde_json",
  "sqs-executor",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tracing",
  "tracing-futures",
  "tracing-subscriber",
@@ -1706,7 +1707,6 @@ dependencies = [
  "base64 0.13.0",
  "chrono",
  "grapl-config",
- "grapl-graph-descriptions",
  "grapl-observe",
  "grapl-service",
  "log",
@@ -1715,11 +1715,12 @@ dependencies = [
  "rusoto_credential",
  "rusoto_s3",
  "rusoto_sqs",
+ "rust-proto",
  "serde",
  "serde_json",
  "sha2",
  "sqs-executor",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tracing",
  "zstd",
 ]
@@ -1735,10 +1736,9 @@ dependencies = [
  "dgraph-tonic",
  "dgraph_query_lib",
  "failure",
- "futures 0.3.16",
+ "futures 0.3.17",
  "futures-retry",
  "grapl-config",
- "grapl-graph-descriptions",
  "grapl-observe",
  "grapl-service",
  "grapl-utils",
@@ -1752,13 +1752,14 @@ dependencies = [
  "rusoto_dynamodb",
  "rusoto_s3",
  "rusoto_sqs",
+ "rust-proto",
  "serde",
  "serde_derive",
  "serde_json",
  "sha2",
  "sqs-executor",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tracing",
  "tracing-futures",
  "tracing-subscriber",
@@ -1783,7 +1784,7 @@ dependencies = [
  "derive-dynamic-node",
  "graph-generator-lib",
  "grapl-config",
- "grapl-graph-descriptions",
+ "rust-proto",
 ]
 
 [[package]]
@@ -1802,31 +1803,13 @@ dependencies = [
  "rusoto_s3",
  "rusoto_sqs",
  "sqs-executor",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tracing",
  "tracing-appender",
  "tracing-futures",
  "tracing-subscriber",
 ]
 
-[[package]]
-name = "grapl-graph-descriptions"
-version = "0.2.10"
-dependencies = [
- "log",
- "prost",
- "prost-build",
- "prost-derive",
- "prost-types",
- "quickcheck",
- "quickcheck_macros",
- "rand 0.8.4",
- "serde",
- "serde_derive",
- "tracing",
- "tracing-subscriber",
-]
-
 [[package]]
 name = "grapl-graphql-codegen"
 version = "0.1.0"
@@ -1857,12 +1840,12 @@ dependencies = [
 name = "grapl-service"
 version = "0.1.0"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "grapl-config",
- "grapl-graph-descriptions",
  "itertools 0.10.1",
  "libflate",
  "prost",
+ "rust-proto",
  "serde",
  "serde_json",
  "sqs-executor",
@@ -1878,7 +1861,7 @@ dependencies = [
  "async-trait",
  "rusoto_core",
  "rusoto_dynamodb",
- "tokio 1.10.0",
+ "tokio 1.12.0",
 ]
 
 [[package]]
@@ -1890,7 +1873,7 @@ dependencies = [
  "actix-web",
  "argon2",
  "chrono",
- "futures 0.3.16",
+ "futures 0.3.17",
  "futures-util",
  "grapl-config",
  "hmap",
@@ -1901,7 +1884,7 @@ dependencies = [
  "serde_dynamodb 0.8.0",
  "tap",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tracing",
  "tracing-subscriber",
  "url",
@@ -1936,7 +1919,7 @@ dependencies = [
  "futures-core",
  "futures-sink",
  "futures-util",
- "http 0.2.4",
+ "http 0.2.5",
  "indexmap",
  "slab",
  "tokio 0.2.25",
@@ -1947,20 +1930,20 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.3.3"
+version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726"
+checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "fnv",
  "futures-core",
  "futures-sink",
  "futures-util",
- "http 0.2.4",
+ "http 0.2.5",
  "indexmap",
  "slab",
- "tokio 1.10.0",
- "tokio-util 0.6.7",
+ "tokio 1.12.0",
+ "tokio-util 0.6.8",
  "tracing",
 ]
 
@@ -2053,11 +2036,11 @@ dependencies = [
 
 [[package]]
 name = "http"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
+checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "fnv",
  "itoa",
 ]
@@ -2080,8 +2063,8 @@ version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5"
 dependencies = [
- "bytes 1.0.1",
- "http 0.2.4",
+ "bytes 1.1.0",
+ "http 0.2.5",
  "pin-project-lite 0.2.7",
 ]
 
@@ -2091,15 +2074,15 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25698ac7002625796d5a7df8f0602400571da944c2edc1d5c268a6947dd4f692"
 dependencies = [
- "http 0.2.4",
+ "http 0.2.5",
  "serde",
 ]
 
 [[package]]
 name = "httparse"
-version = "1.4.1"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68"
+checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
 
 [[package]]
 name = "httpdate"
@@ -2139,23 +2122,23 @@ dependencies = [
 
 [[package]]
 name = "hyper"
-version = "0.14.11"
+version = "0.14.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11"
+checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "futures-channel",
  "futures-core",
  "futures-util",
- "h2 0.3.3",
- "http 0.2.4",
+ "h2 0.3.4",
+ "http 0.2.5",
  "http-body 0.4.3",
  "httparse",
  "httpdate",
  "itoa",
  "pin-project-lite 0.2.7",
- "socket2 0.4.1",
- "tokio 1.10.0",
+ "socket2 0.4.2",
+ "tokio 1.12.0",
  "tower-service",
  "tracing",
  "want 0.3.0",
@@ -2169,11 +2152,11 @@ checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64"
 dependencies = [
  "ct-logs",
  "futures-util",
- "hyper 0.14.11",
+ "hyper 0.14.13",
  "log",
  "rustls",
  "rustls-native-certs",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tokio-rustls",
  "webpki",
 ]
@@ -2261,24 +2244,24 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "0.4.7"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
 
 [[package]]
 name = "jobserver"
-version = "0.1.23"
+version = "0.1.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5ca711fd837261e14ec9e674f092cbb931d3fa1482b017ae59328ddc6f3212b"
+checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "js-sys"
-version = "0.3.52"
+version = "0.3.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752"
+checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -2292,7 +2275,7 @@ dependencies = [
  "prost-build",
  "quanta",
  "rdkafka",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tokio-stream",
  "tracing",
  "tracing-subscriber",
@@ -2395,15 +2378,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.99"
+version = "0.2.102"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
+checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
 
 [[package]]
 name = "libflate"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d87eae36b3f680f7f01645121b782798b56ef33c53f83d1c66ba3a22b60bfe3"
+checksum = "16364af76ebb39b5869bb32c81fa93573267cd8c62bb3474e28d78fac3fb141e"
 dependencies = [
  "adler32",
  "crc32fast",
@@ -2448,9 +2431,9 @@ dependencies = [
 
 [[package]]
 name = "lock_api"
-version = "0.4.4"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
+checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
 dependencies = [
  "scopeguard",
 ]
@@ -2526,9 +2509,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
 
 [[package]]
 name = "memchr"
-version = "2.4.0"
+version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
 
 [[package]]
 name = "memoffset"
@@ -2557,7 +2540,7 @@ dependencies = [
  "base64 0.13.0",
  "chrono",
  "flate2",
- "futures 0.3.16",
+ "futures 0.3.17",
  "grapl-config",
  "hmap",
  "itertools 0.10.1",
@@ -2570,7 +2553,7 @@ dependencies = [
  "serde_json",
  "statsd-parser",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tokio-compat",
  "tokio-compat-02",
  "uuid",
@@ -2595,10 +2578,10 @@ checksum = "caa72e4a3d157986dd2565c82ecbddcc23941513669a3766b938f6b72eb87f3f"
 dependencies = [
  "lazy_static",
  "proc-macro-hack",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
  "regex",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -2714,18 +2697,18 @@ version = "1.0.0"
 dependencies = [
  "async-trait",
  "base64 0.13.0",
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "chrono",
  "failure",
- "futures 0.3.16",
+ "futures 0.3.17",
  "grapl-config",
- "grapl-graph-descriptions",
  "grapl-observe",
  "grapl-service",
  "grapl-utils",
  "hex",
  "hmap",
  "prost",
+ "prost-build",
  "quickcheck",
  "quickcheck_macros",
  "rusoto_core",
@@ -2733,6 +2716,7 @@ dependencies = [
  "rusoto_dynamodb",
  "rusoto_s3",
  "rusoto_sqs",
+ "rust-proto",
  "serde",
  "serde_derive",
  "serde_dynamodb 0.7.0",
@@ -2741,7 +2725,7 @@ dependencies = [
  "sqs-executor",
  "tap",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tracing",
  "uuid",
  "zstd",
@@ -2797,9 +2781,9 @@ dependencies = [
 
 [[package]]
 name = "num_enum"
-version = "0.5.3"
+version = "0.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee2c8fd66061a707503d515639b8af10fd3807a5b5ee6959f7ff1bd303634bd5"
+checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f"
 dependencies = [
  "derivative",
  "num_enum_derive",
@@ -2807,21 +2791,21 @@ dependencies = [
 
 [[package]]
 name = "num_enum_derive"
-version = "0.5.3"
+version = "0.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "474fd1d096da3ad17084694eebed40ba09c4a36c5255cd772bd8b98859cc562e"
+checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9"
 dependencies = [
  "proc-macro-crate",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
 name = "object"
-version = "0.26.0"
+version = "0.26.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386"
+checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2"
 dependencies = [
  "memchr",
 ]
@@ -2852,9 +2836,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
 
 [[package]]
 name = "ordered-float"
-version = "2.7.0"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "039f02eb0f69271f26abe3202189275d7aa2258b903cb0281b5de710a2570ff3"
+checksum = "97c9d06878b3a851e8026ef94bf7fef9ba93062cd412601da4d9cf369b1cc62d"
 dependencies = [
  "num-traits",
 ]
@@ -2868,14 +2852,14 @@ dependencies = [
  "endpoint-plugin",
  "graph-generator-lib",
  "grapl-config",
- "grapl-graph-descriptions",
  "grapl-observe",
  "grapl-service",
+ "rust-proto",
  "serde",
  "serde_json",
  "sqs-executor",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tracing",
 ]
 
@@ -2898,13 +2882,13 @@ dependencies = [
 
 [[package]]
 name = "parking_lot"
-version = "0.11.1"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
 dependencies = [
  "instant",
- "lock_api 0.4.4",
- "parking_lot_core 0.8.3",
+ "lock_api 0.4.5",
+ "parking_lot_core 0.8.5",
 ]
 
 [[package]]
@@ -2924,9 +2908,9 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.8.3"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
 dependencies = [
  "cfg-if 1.0.0",
  "instant",
@@ -2938,9 +2922,9 @@ dependencies = [
 
 [[package]]
 name = "password-hash"
-version = "0.2.2"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd482dfb8cfba5a93ec0f91e1c0f66967cb2fdc1a8dba646c4f9202c5d05d785"
+checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7"
 dependencies = [
  "base64ct",
  "rand_core 0.6.3",
@@ -2996,9 +2980,9 @@ version = "0.4.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3be26700300be6d9d23264c73211d8190e755b6b5ca7a1b28230025511b52a5e"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -3007,9 +2991,9 @@ version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -3083,9 +3067,9 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
 
 [[package]]
 name = "proc-macro-crate"
-version = "1.0.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92"
+checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83"
 dependencies = [
  "thiserror",
  "toml",
@@ -3098,9 +3082,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
 dependencies = [
  "proc-macro-error-attr",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
  "version_check 0.9.3",
 ]
 
@@ -3110,7 +3094,7 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
  "version_check 0.9.3",
 ]
@@ -3138,9 +3122,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.28"
+version = "1.0.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
+checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
 dependencies = [
  "unicode-xid 0.2.2",
 ]
@@ -3151,7 +3135,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e6984d2f1a23009bd270b8bb56d0926810a3d483f59c987d77969e9d8e840b2"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "prost-derive",
 ]
 
@@ -3161,7 +3145,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32d3ebd75ac2679c2af3a92246639f9fcc8a442ee420719cc4fe195b98dd5fa3"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "heck",
  "itertools 0.9.0",
  "log",
@@ -3181,9 +3165,9 @@ checksum = "169a15f3008ecb5160cba7d37bcd690a7601b6d30cfb87a117d45e59d52af5d4"
 dependencies = [
  "anyhow",
  "itertools 0.9.0",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -3192,7 +3176,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b518d7cdd93dab1d1122cf07fa9a60771836c668dde9d9e2a139f957f0d9f1bb"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "prost",
 ]
 
@@ -3234,9 +3218,9 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -3260,7 +3244,7 @@ version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
 ]
 
 [[package]]
@@ -3384,7 +3368,7 @@ version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "af78bc431a82ef178c4ad6db537eb9cc25715a8591d27acc30455ee7227a76f4"
 dependencies = [
- "futures 0.3.16",
+ "futures 0.3.17",
  "libc",
  "log",
  "rdkafka-sys",
@@ -3392,7 +3376,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "slab",
- "tokio 1.10.0",
+ "tokio 1.12.0",
 ]
 
 [[package]]
@@ -3415,17 +3399,17 @@ checksum = "d4f0ceb2ec0dd769483ecd283f6615aa83dcd0be556d5294c6e659caefe7cc54"
 dependencies = [
  "arc-swap",
  "async-trait",
- "bytes 1.0.1",
- "combine 4.6.0",
+ "bytes 1.1.0",
+ "combine 4.6.1",
  "dtoa",
- "futures 0.3.16",
+ "futures 0.3.17",
  "futures-util",
  "itoa",
  "percent-encoding",
  "pin-project-lite 0.2.7",
  "sha1",
- "tokio 1.10.0",
- "tokio-util 0.6.7",
+ "tokio 1.12.0",
+ "tokio-util 0.6.8",
  "url",
 ]
 
@@ -3527,8 +3511,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "17dc69132670b25e3cc2f5cdb3a56605615c19f563be3d6ee5e359ccfa400fa8"
 dependencies = [
  "async-trait",
- "bytes 1.0.1",
- "futures 0.3.16",
+ "bytes 1.1.0",
+ "futures 0.3.17",
  "rusoto_core",
  "serde_urlencoded 0.6.1",
  "xml-rs",
@@ -3542,11 +3526,11 @@ checksum = "02aff20978970d47630f08de5f0d04799497818d16cafee5aec90c4b4d0806cf"
 dependencies = [
  "async-trait",
  "base64 0.13.0",
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "crc32fast",
- "futures 0.3.16",
- "http 0.2.4",
- "hyper 0.14.11",
+ "futures 0.3.17",
+ "http 0.2.5",
+ "hyper 0.14.13",
  "hyper-rustls",
  "lazy_static",
  "log",
@@ -3555,7 +3539,7 @@ dependencies = [
  "rustc_version 0.2.3",
  "serde",
  "serde_json",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "xml-rs",
 ]
 
@@ -3568,12 +3552,12 @@ dependencies = [
  "async-trait",
  "chrono",
  "dirs-next",
- "futures 0.3.16",
- "hyper 0.14.11",
+ "futures 0.3.17",
+ "hyper 0.14.13",
  "serde",
  "serde_json",
  "shlex",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "zeroize",
 ]
 
@@ -3584,8 +3568,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0f26af40f36409cb8fae3069690f78f638f747b55c7b90f338d5ed36016b0cda"
 dependencies = [
  "async-trait",
- "bytes 1.0.1",
- "futures 0.3.16",
+ "bytes 1.1.0",
+ "futures 0.3.17",
  "rusoto_core",
  "serde",
  "serde_json",
@@ -3598,8 +3582,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "abc3f56f14ccf91f880b9a9c2d0556d8523e8c155041c54db155b384a1dd1119"
 dependencies = [
  "async-trait",
- "bytes 1.0.1",
- "futures 0.3.16",
+ "bytes 1.1.0",
+ "futures 0.3.17",
  "rusoto_core",
  "xml-rs",
 ]
@@ -3611,12 +3595,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5486e6b1673ab3e0ba1ded284fb444845fe1b7f41d13989a54dd60f62a7b2baa"
 dependencies = [
  "base64 0.13.0",
- "bytes 1.0.1",
- "futures 0.3.16",
+ "bytes 1.1.0",
+ "futures 0.3.17",
  "hex",
  "hmac",
- "http 0.2.4",
- "hyper 0.14.11",
+ "http 0.2.5",
+ "hyper 0.14.13",
  "log",
  "md5",
  "percent-encoding",
@@ -3626,7 +3610,7 @@ dependencies = [
  "serde",
  "sha2",
  "time 0.2.27",
- "tokio 1.10.0",
+ "tokio 1.12.0",
 ]
 
 [[package]]
@@ -3636,18 +3620,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "054901b35dd1ed5f7be0e490a9d3476dc0b236ca8383fe600d3fe851e0a32e67"
 dependencies = [
  "async-trait",
- "bytes 1.0.1",
- "futures 0.3.16",
+ "bytes 1.1.0",
+ "futures 0.3.17",
  "rusoto_core",
  "serde_urlencoded 0.6.1",
  "xml-rs",
 ]
 
+[[package]]
+name = "rust-proto"
+version = "0.2.10"
+dependencies = [
+ "log",
+ "prost",
+ "prost-build",
+ "prost-derive",
+ "prost-types",
+ "quickcheck",
+ "quickcheck_macros",
+ "rand 0.8.4",
+ "serde",
+ "serde_derive",
+ "tracing",
+ "tracing-subscriber",
+ "uuid",
+]
+
 [[package]]
 name = "rustc-demangle"
-version = "0.1.20"
+version = "0.1.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49"
+checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
 
 [[package]]
 name = "rustc_version"
@@ -3744,9 +3747,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework"
-version = "2.3.1"
+version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
+checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
 dependencies = [
  "bitflags",
  "core-foundation",
@@ -3757,9 +3760,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework-sys"
-version = "2.3.0"
+version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284"
+checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
 dependencies = [
  "core-foundation-sys",
  "libc",
@@ -3806,9 +3809,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.127"
+version = "1.0.130"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
 dependencies = [
  "serde_derive",
 ]
@@ -3837,13 +3840,13 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.127"
+version = "1.0.130"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -3852,7 +3855,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e9faf5eb2ed31361096d8273a89862d506fe6c6178d86cfb181a3b86e533405"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "rusoto_dynamodb",
  "serde",
 ]
@@ -3863,16 +3866,16 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "43fc25c1d01ec67946b2088615e2c82c19c84321bafde401dd8f42e7232ad9de"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "rusoto_dynamodb",
  "serde",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.66"
+version = "1.0.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
+checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
 dependencies = [
  "itoa",
  "ryu",
@@ -3905,9 +3908,9 @@ dependencies = [
 
 [[package]]
 name = "sha-1"
-version = "0.9.7"
+version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81"
+checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
 dependencies = [
  "block-buffer",
  "cfg-if 1.0.0",
@@ -3924,9 +3927,9 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
 
 [[package]]
 name = "sha2"
-version = "0.9.5"
+version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
+checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
 dependencies = [
  "block-buffer",
  "cfg-if 1.0.0",
@@ -3993,9 +3996,9 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.4.1"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad"
+checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
 dependencies = [
  "libc",
  "winapi 0.3.9",
@@ -4013,7 +4016,7 @@ version = "0.1.0"
 dependencies = [
  "async-trait",
  "chrono",
- "futures 0.3.16",
+ "futures 0.3.17",
  "futures-util",
  "grapl-observe",
  "grapl-utils",
@@ -4027,11 +4030,12 @@ dependencies = [
  "rusoto_core",
  "rusoto_s3",
  "rusoto_sqs",
+ "rust-proto",
  "serde",
  "serde_json",
  "tap",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tracing",
  "tracing-futures",
  "uuid",
@@ -4072,11 +4076,11 @@ version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
  "serde",
  "serde_derive",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -4086,13 +4090,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
 dependencies = [
  "base-x",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
  "serde",
  "serde_derive",
  "serde_json",
  "sha1",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -4124,9 +4128,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
 
 [[package]]
 name = "structopt"
-version = "0.3.22"
+version = "0.3.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69b041cdcb67226aca307e6e7be44c8806423d83e018bd662360a93dabce4d71"
+checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa"
 dependencies = [
  "clap",
  "lazy_static",
@@ -4135,15 +4139,15 @@ dependencies = [
 
 [[package]]
 name = "structopt-derive"
-version = "0.4.15"
+version = "0.4.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7813934aecf5f51a54775e00068c237de98489463968231a51746bbbc03f9c10"
+checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba"
 dependencies = [
  "heck",
  "proc-macro-error",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -4176,11 +4180,11 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.74"
+version = "1.0.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
+checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
  "unicode-xid 0.2.2",
 ]
@@ -4212,9 +4216,9 @@ version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
  "unicode-xid 0.2.2",
 ]
 
@@ -4241,15 +4245,15 @@ dependencies = [
  "endpoint-plugin",
  "graph-generator-lib",
  "grapl-config",
- "grapl-graph-descriptions",
  "grapl-observe",
  "grapl-service",
+ "rust-proto",
  "serde",
  "serde_json",
  "sqs-executor",
  "sysmon",
  "thiserror",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tracing",
  "uuid",
 ]
@@ -4297,22 +4301,22 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.26"
+version = "1.0.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
+checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.26"
+version = "1.0.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
+checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -4375,10 +4379,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
 dependencies = [
  "proc-macro-hack",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
  "standback",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -4393,9 +4397,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.3.1"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338"
+checksum = "5241dd6f21443a3606b432718b166d3cedc962fd4b8bea54a8bc7f514ebda986"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -4454,18 +4458,18 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.10.0"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b"
+checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
 dependencies = [
  "autocfg",
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "libc",
  "memchr",
  "mio 0.7.13",
  "num_cpus",
  "once_cell",
- "parking_lot 0.11.1",
+ "parking_lot 0.11.2",
  "pin-project-lite 0.2.7",
  "signal-hook-registry",
  "tokio-macros",
@@ -4521,7 +4525,7 @@ dependencies = [
  "once_cell",
  "pin-project-lite 0.2.7",
  "tokio 0.2.25",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tokio-stream",
 ]
 
@@ -4573,9 +4577,9 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -4604,7 +4608,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
 dependencies = [
  "rustls",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "webpki",
 ]
 
@@ -4616,7 +4620,7 @@ checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
 dependencies = [
  "futures-core",
  "pin-project-lite 0.2.7",
- "tokio 1.10.0",
+ "tokio 1.12.0",
 ]
 
 [[package]]
@@ -4721,16 +4725,16 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.6.7"
+version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
+checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd"
 dependencies = [
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "futures-core",
  "futures-sink",
  "log",
  "pin-project-lite 0.2.7",
- "tokio 1.10.0",
+ "tokio 1.12.0",
 ]
 
 [[package]]
@@ -4751,21 +4755,21 @@ dependencies = [
  "async-stream 0.3.2",
  "async-trait",
  "base64 0.13.0",
- "bytes 1.0.1",
+ "bytes 1.1.0",
  "futures-core",
  "futures-util",
- "h2 0.3.3",
- "http 0.2.4",
+ "h2 0.3.4",
+ "http 0.2.5",
  "http-body 0.4.3",
- "hyper 0.14.11",
+ "hyper 0.14.13",
  "percent-encoding",
  "pin-project 1.0.8",
  "prost",
  "prost-derive",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tokio-rustls",
  "tokio-stream",
- "tokio-util 0.6.7",
+ "tokio-util 0.6.8",
  "tower",
  "tower-service",
  "tracing",
@@ -4778,10 +4782,10 @@ version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c695de27302f4697191dda1c7178131a8cb805463dda02864acb80fe1322fdcf"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "prost-build",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -4796,9 +4800,9 @@ dependencies = [
  "pin-project 1.0.8",
  "rand 0.8.4",
  "slab",
- "tokio 1.10.0",
+ "tokio 1.12.0",
  "tokio-stream",
- "tokio-util 0.6.7",
+ "tokio-util 0.6.8",
  "tower-layer",
  "tower-service",
  "tracing",
@@ -4818,9 +4822,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
 
 [[package]]
 name = "tracing"
-version = "0.1.26"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
+checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8"
 dependencies = [
  "cfg-if 1.0.0",
  "log",
@@ -4842,20 +4846,20 @@ dependencies = [
 
 [[package]]
 name = "tracing-attributes"
-version = "0.1.15"
+version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2"
+checksum = "98863d0dd09fa59a1b79c6750ad80dbda6b75f4e71c437a6a1a8cb91a8bcbd77"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
 name = "tracing-core"
-version = "0.1.18"
+version = "0.1.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052"
+checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf"
 dependencies = [
  "lazy_static",
 ]
@@ -4903,9 +4907,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-subscriber"
-version = "0.2.19"
+version = "0.2.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab69019741fca4d98be3c62d2b75254528b5432233fd8a4d2739fec20278de48"
+checksum = "fdd0568dbfe3baf7048b7908d2b32bca0d81cd56bec6d2a8f894b01d74f86be3"
 dependencies = [
  "ansi_term 0.12.1",
  "chrono",
@@ -4932,7 +4936,7 @@ dependencies = [
  "async-trait",
  "cfg-if 1.0.0",
  "enum-as-inner",
- "futures 0.3.16",
+ "futures 0.3.17",
  "idna",
  "lazy_static",
  "log",
@@ -4950,7 +4954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "710f593b371175db53a26d0b38ed2978fafb9e9e8d3868b1acd753ea18df0ceb"
 dependencies = [
  "cfg-if 0.1.10",
- "futures 0.3.16",
+ "futures 0.3.17",
  "ipconfig",
  "lazy_static",
  "log",
@@ -4970,9 +4974,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
 [[package]]
 name = "typenum"
-version = "1.13.0"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
+checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
 
 [[package]]
 name = "ucd-trie"
@@ -5012,9 +5016,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
 
 [[package]]
 name = "unicode-width"
-version = "0.1.8"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
 
 [[package]]
 name = "unicode-xid"
@@ -5098,9 +5102,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c860ad1273f4eee7006cee05db20c9e60e5d24cba024a32e1094aa8e574f3668"
 dependencies = [
  "nom",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
 ]
 
 [[package]]
@@ -5189,9 +5193,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.75"
+version = "0.2.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586"
+checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
 dependencies = [
  "cfg-if 1.0.0",
  "wasm-bindgen-macro",
@@ -5199,24 +5203,24 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.75"
+version = "0.2.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f"
+checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
 dependencies = [
  "bumpalo",
  "lazy_static",
  "log",
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.75"
+version = "0.2.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c"
+checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
 dependencies = [
  "quote 1.0.9",
  "wasm-bindgen-macro-support",
@@ -5224,28 +5228,28 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.75"
+version = "0.2.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f"
+checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
 dependencies = [
- "proc-macro2 1.0.28",
+ "proc-macro2 1.0.29",
  "quote 1.0.9",
- "syn 1.0.74",
+ "syn 1.0.76",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.75"
+version = "0.2.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2"
+checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
 
 [[package]]
 name = "web-sys"
-version = "0.3.52"
+version = "0.3.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696"
+checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -5348,9 +5352,9 @@ checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
 
 [[package]]
 name = "zeroize"
-version = "1.4.1"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "377db0846015f7ae377174787dd452e1c5f5a9050bc6f954911d01f116daa0cd"
+checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970"
 
 [[package]]
 name = "zstd"
diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml
index 237948092e..01980f5c30 100644
--- a/src/rust/Cargo.toml
+++ b/src/rust/Cargo.toml
@@ -7,7 +7,6 @@ members = [
   "./generators/graph-generator-lib",
   "./generators/osquery-generator",
   "./generators/sysmon-generator",
-  "./graph-descriptions",
   "./graph-merger",
   "./grapl",
   "./grapl-config",
@@ -19,6 +18,7 @@ members = [
   "./kafka-metrics-exporter",
   "./metric-forwarder",
   "./node-identifier",
+  "./rust-proto",
   "./sqs-executor",
   "./sysmon",
 ]
diff --git a/src/rust/analyzer-dispatcher/Cargo.toml b/src/rust/analyzer-dispatcher/Cargo.toml
index edef8a9d3b..b244744519 100644
--- a/src/rust/analyzer-dispatcher/Cargo.toml
+++ b/src/rust/analyzer-dispatcher/Cargo.toml
@@ -5,7 +5,7 @@ authors = ["Insanitybit <insanitybit@gmail.com>"]
 edition = "2018"
 
 [dependencies]
-grapl-graph-descriptions = { path = "../graph-descriptions", version = "*" }
+rust-proto = { path = "../rust-proto", version = "*" }
 grapl-config = { path = "../grapl-config", version = "*" }
 grapl-observe = { path = "../grapl-observe" }
 grapl-service = { path = "../grapl-service" }
diff --git a/src/rust/analyzer-dispatcher/src/dispatch_event.rs b/src/rust/analyzer-dispatcher/src/dispatch_event.rs
index f6ddb8dfc9..cc494eb523 100644
--- a/src/rust/analyzer-dispatcher/src/dispatch_event.rs
+++ b/src/rust/analyzer-dispatcher/src/dispatch_event.rs
@@ -1,5 +1,8 @@
-use grapl_graph_descriptions::graph_description::*;
 use prost::Message;
+use rust_proto::{
+    graph_descriptions::*,
+    pipeline::ServiceMessage,
+};
 use serde::{
     Deserialize,
     Serialize,
@@ -19,6 +22,33 @@ impl AnalyzerDispatchEvent {
     }
 }
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AnalyzerDispatchEvents {
+    events: Vec<AnalyzerDispatchEvent>,
+}
+
+impl AnalyzerDispatchEvents {
+    pub fn new() -> Self {
+        Self { events: vec![] }
+    }
+}
+
+impl Default for AnalyzerDispatchEvents {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl From<Vec<AnalyzerDispatchEvent>> for AnalyzerDispatchEvents {
+    fn from(events: Vec<AnalyzerDispatchEvent>) -> Self {
+        Self { events }
+    }
+}
+
+impl ServiceMessage for AnalyzerDispatchEvents {
+    const TYPE_NAME: &'static str = "AnalyzerDispatchEvent";
+}
+
 #[derive(thiserror::Error, Debug)]
 pub enum DispatchEventEncoderError {
     #[error("IO")]
@@ -35,7 +65,7 @@ pub struct AnalyzerDispatchSerializer {
 }
 
 impl CompletionEventSerializer for AnalyzerDispatchSerializer {
-    type CompletedEvent = Vec<AnalyzerDispatchEvent>;
+    type CompletedEvent = AnalyzerDispatchEvents;
     type Output = Vec<u8>;
     type Error = DispatchEventEncoderError;
 
@@ -43,7 +73,11 @@ impl CompletionEventSerializer for AnalyzerDispatchSerializer {
         &mut self,
         completed_events: &[Self::CompletedEvent],
     ) -> Result<Vec<Self::Output>, Self::Error> {
-        let unique_events: Vec<_> = completed_events.iter().flatten().collect();
+        let unique_events: Vec<_> = completed_events
+            .iter()
+            .map(|e| &e.events)
+            .flatten()
+            .collect();
 
         let mut final_subgraph = MergedGraph::new();
 
diff --git a/src/rust/analyzer-dispatcher/src/main.rs b/src/rust/analyzer-dispatcher/src/main.rs
index 9b753aa3dd..416c230ec8 100644
--- a/src/rust/analyzer-dispatcher/src/main.rs
+++ b/src/rust/analyzer-dispatcher/src/main.rs
@@ -12,7 +12,6 @@ use grapl_config::env_helpers::{
     s3_event_emitters_from_env,
     FromEnv,
 };
-use grapl_graph_descriptions::graph_description::*;
 use grapl_observe::metric_reporter::MetricReporter;
 use grapl_service::decoder::ProtoDecoder;
 use log::{
@@ -27,6 +26,7 @@ use rusoto_s3::{
     S3,
 };
 use rusoto_sqs::SqsClient;
+use rust_proto::graph_descriptions::*;
 use sqs_executor::{
     cache::NopCache,
     errors::{
@@ -44,6 +44,7 @@ use sqs_executor::{
 
 use crate::dispatch_event::{
     AnalyzerDispatchEvent,
+    AnalyzerDispatchEvents,
     AnalyzerDispatchSerializer,
 };
 
@@ -115,7 +116,7 @@ where
     S: S3 + Send + Sync + 'static,
 {
     type InputEvent = MergedGraph;
-    type OutputEvent = Vec<AnalyzerDispatchEvent>;
+    type OutputEvent = AnalyzerDispatchEvents;
     type Error = AnalyzerDispatcherError;
 
     async fn handle_event(
@@ -127,7 +128,7 @@ where
 
         if subgraph.is_empty() {
             warn!("Attempted to handle empty subgraph");
-            return Ok(vec![]);
+            return Ok(AnalyzerDispatchEvents::new());
         }
 
         info!("Retrieving S3 keys");
@@ -159,11 +160,11 @@ where
 
         if let Some(e) = failed {
             Err(Ok((
-                dispatch_events,
+                AnalyzerDispatchEvents::from(dispatch_events),
                 AnalyzerDispatcherError::Unexpected(e.to_string()),
             )))
         } else {
-            Ok(dispatch_events)
+            Ok(AnalyzerDispatchEvents::from(dispatch_events))
         }
     }
 }
diff --git a/src/rust/derive-dynamic-node/Cargo.toml b/src/rust/derive-dynamic-node/Cargo.toml
index 303f730dc4..50f1e2e084 100644
--- a/src/rust/derive-dynamic-node/Cargo.toml
+++ b/src/rust/derive-dynamic-node/Cargo.toml
@@ -16,7 +16,7 @@ quote = "1"
 proc-macro2 = "1"
 
 [dev-dependencies]
-grapl-graph-descriptions = { path = "../graph-descriptions", version = "*" }
+rust-proto = { path = "../rust-proto", version = "*" }
 serde = "1"
 serde_json = "1"
 serde_derive = "1"
diff --git a/src/rust/derive-dynamic-node/examples/ex.rs b/src/rust/derive-dynamic-node/examples/ex.rs
index e77a72b1a8..750eccba50 100644
--- a/src/rust/derive-dynamic-node/examples/ex.rs
+++ b/src/rust/derive-dynamic-node/examples/ex.rs
@@ -3,8 +3,8 @@ use derive_dynamic_node::{
     GraplStaticId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
 use log::info;
+use rust_proto::graph_descriptions::*;
 use serde_derive::Deserialize;
 
 fn read_log() -> &'static [u8] {
diff --git a/src/rust/derive-dynamic-node/src/lib.rs b/src/rust/derive-dynamic-node/src/lib.rs
index 659b2c44be..504a492634 100644
--- a/src/rust/derive-dynamic-node/src/lib.rs
+++ b/src/rust/derive-dynamic-node/src/lib.rs
@@ -98,21 +98,21 @@ pub fn derive_node_description(input: TokenStream) -> TokenStream {
 
         #[derive(Clone, Debug)]
         pub struct #node_name {
-            dynamic_node: grapl_graph_descriptions::graph_description::NodeDescription,
+            dynamic_node: rust_proto::graph_descriptions::NodeDescription,
         }
 
         pub trait #node_trait_name {
-            fn get_mut_dynamic_node(&mut self) -> &mut grapl_graph_descriptions::graph_description::NodeDescription;
-            fn get_dynamic_node(&self) -> &grapl_graph_descriptions::graph_description::NodeDescription;
+            fn get_mut_dynamic_node(&mut self) -> &mut rust_proto::graph_descriptions::NodeDescription;
+            fn get_dynamic_node(&self) -> &rust_proto::graph_descriptions::NodeDescription;
 
             #methods
         }
 
         impl #node_name {
-            pub fn new(strategy: grapl_graph_descriptions::graph_description::IdStrategy) -> Self {
+            pub fn new(strategy: rust_proto::graph_descriptions::IdStrategy) -> Self {
                 let mut properties = std::collections::HashMap::with_capacity(1);
 
-                let dynamic_node = grapl_graph_descriptions::graph_description::NodeDescription {
+                let dynamic_node = rust_proto::graph_descriptions::NodeDescription {
                     node_type: #struct_name_string .to_owned(),
                     id_strategy: vec![strategy],
                     node_key: uuid::Uuid::new_v4().to_string(),
@@ -135,20 +135,20 @@ pub fn derive_node_description(input: TokenStream) -> TokenStream {
             }
         }
 
-        impl AsRef<grapl_graph_descriptions::graph_description::NodeDescription> for #node_name {
+        impl AsRef<rust_proto::graph_descriptions::NodeDescription> for #node_name {
             fn as_ref(&self) -> &NodeDescription {
                 &self.dynamic_node
             }
         }
 
-        impl AsMut<grapl_graph_descriptions::graph_description::NodeDescription> for #node_name {
+        impl AsMut<rust_proto::graph_descriptions::NodeDescription> for #node_name {
             fn as_mut(&mut self) -> &mut NodeDescription {
                 &mut self.dynamic_node
             }
         }
 
-        impl From<#node_name> for grapl_graph_descriptions::graph_description::NodeDescription {
-            fn from(n: #node_name) -> grapl_graph_descriptions::graph_description::NodeDescription {
+        impl From<#node_name> for rust_proto::graph_descriptions::NodeDescription {
+            fn from(n: #node_name) -> rust_proto::graph_descriptions::NodeDescription {
                 n.dynamic_node
             }
         }
@@ -350,35 +350,35 @@ fn resolvable_type_from(
             match (typepath.as_ref(), resolution_name) {
                 /* underlying struct field type    maps to this type   via this method on NodeProperty */
                 ("String", IMMUTABLE) => (
-                    parse_quote!(grapl_graph_descriptions::ImmutableStrProp),
+                    parse_quote!(rust_proto::graph_descriptions::ImmutableStrProp),
                     parse_quote!(as_immutable_str),
                 ),
                 ("std::string::String", IMMUTABLE) => (
-                    parse_quote!(grapl_graph_descriptions::ImmutableStrProp),
+                    parse_quote!(rust_proto::graph_descriptions::ImmutableStrProp),
                     parse_quote!(as_immutable_str),
                 ),
                 ("u64", IMMUTABLE) => (
-                    parse_quote!(grapl_graph_descriptions::ImmutableUintProp),
+                    parse_quote!(rust_proto::graph_descriptions::ImmutableUintProp),
                     parse_quote!(as_immutable_uint),
                 ),
                 ("u64", INCREMENT) => (
-                    parse_quote!(grapl_graph_descriptions::IncrementOnlyUintProp),
+                    parse_quote!(rust_proto::graph_descriptions::IncrementOnlyUintProp),
                     parse_quote!(as_increment_only_uint),
                 ),
                 ("u64", DECREMENT) => (
-                    parse_quote!(grapl_graph_descriptions::DecrementOnlyUintProp),
+                    parse_quote!(rust_proto::graph_descriptions::DecrementOnlyUintProp),
                     parse_quote!(as_decrement_only_uint),
                 ),
                 ("i64", IMMUTABLE) => (
-                    parse_quote!(grapl_graph_descriptions::ImmutableIntProp),
+                    parse_quote!(rust_proto::graph_descriptions::ImmutableIntProp),
                     parse_quote!(as_immutable_int),
                 ),
                 ("i64", INCREMENT) => (
-                    parse_quote!(grapl_graph_descriptions::IncrementOnlyIntProp),
+                    parse_quote!(rust_proto::graph_descriptions::IncrementOnlyIntProp),
                     parse_quote!(as_increment_only_int),
                 ),
                 ("i64", DECREMENT) => (
-                    parse_quote!(grapl_graph_descriptions::DecrementOnlyIntProp),
+                    parse_quote!(rust_proto::graph_descriptions::DecrementOnlyIntProp),
                     parse_quote!(as_decrement_only_int),
                 ),
                 _ => return None,
@@ -426,8 +426,8 @@ fn identity_prop_setter(field: &Field, property_name: &Ident) -> TS2 {
     quote!(
         let mut self_strategy = mut_self.id_strategy[0].strategy.as_mut().unwrap();
         match self_strategy {
-            grapl_graph_descriptions::graph_description::id_strategy::Strategy::Session(
-                grapl_graph_descriptions::graph_description::Session{ref mut #ident, ..}
+            rust_proto::graph_descriptions::id_strategy::Strategy::Session(
+                rust_proto::graph_descriptions::Session{ref mut #ident, ..}
             ) => {
                 * #ident = #property_name.as_inner();
             }
diff --git a/src/rust/endpoint-plugin/Cargo.toml b/src/rust/endpoint-plugin/Cargo.toml
index 5440672707..85f7429692 100644
--- a/src/rust/endpoint-plugin/Cargo.toml
+++ b/src/rust/endpoint-plugin/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2018"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [dependencies]
-grapl-graph-descriptions = { path = "../graph-descriptions", version = "*" }
+rust-proto = { path = "../rust-proto", version = "*" }
 derive-dynamic-node = { path = "../derive-dynamic-node" }
 uuid = { version = "0.8", features = ["v4"] }
 thiserror = "1.0"
diff --git a/src/rust/endpoint-plugin/src/asset.rs b/src/rust/endpoint-plugin/src/asset.rs
index eb01cecddb..8304d57219 100644
--- a/src/rust/endpoint-plugin/src/asset.rs
+++ b/src/rust/endpoint-plugin/src/asset.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplStaticId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplStaticId)]
 pub struct Asset {
diff --git a/src/rust/endpoint-plugin/src/file.rs b/src/rust/endpoint-plugin/src/file.rs
index cdb8236f3d..08e384f969 100644
--- a/src/rust/endpoint-plugin/src/file.rs
+++ b/src/rust/endpoint-plugin/src/file.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplSessionId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplSessionId)]
 pub struct File {
diff --git a/src/rust/endpoint-plugin/src/ip_address.rs b/src/rust/endpoint-plugin/src/ip_address.rs
index b24322e88e..8c1bf474e0 100644
--- a/src/rust/endpoint-plugin/src/ip_address.rs
+++ b/src/rust/endpoint-plugin/src/ip_address.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplStaticId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplStaticId)]
 pub struct IpAddress {
diff --git a/src/rust/endpoint-plugin/src/ip_connection.rs b/src/rust/endpoint-plugin/src/ip_connection.rs
index b147ba04f1..b0463643e2 100644
--- a/src/rust/endpoint-plugin/src/ip_connection.rs
+++ b/src/rust/endpoint-plugin/src/ip_connection.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplSessionId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplSessionId)]
 pub struct IpConnection {
diff --git a/src/rust/endpoint-plugin/src/ip_port.rs b/src/rust/endpoint-plugin/src/ip_port.rs
index 085c09b7ef..e85d02d35b 100644
--- a/src/rust/endpoint-plugin/src/ip_port.rs
+++ b/src/rust/endpoint-plugin/src/ip_port.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplStaticId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplStaticId)]
 pub struct IpPort {
diff --git a/src/rust/endpoint-plugin/src/network_connection.rs b/src/rust/endpoint-plugin/src/network_connection.rs
index 2ff52b346c..f502b004a2 100644
--- a/src/rust/endpoint-plugin/src/network_connection.rs
+++ b/src/rust/endpoint-plugin/src/network_connection.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplSessionId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplSessionId)]
 pub struct NetworkConnection {
diff --git a/src/rust/endpoint-plugin/src/process.rs b/src/rust/endpoint-plugin/src/process.rs
index 348abab584..1a6975b924 100644
--- a/src/rust/endpoint-plugin/src/process.rs
+++ b/src/rust/endpoint-plugin/src/process.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplSessionId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplSessionId)]
 pub struct Process {
diff --git a/src/rust/endpoint-plugin/src/process_inbound_connection.rs b/src/rust/endpoint-plugin/src/process_inbound_connection.rs
index e22a5238bc..4535423f54 100644
--- a/src/rust/endpoint-plugin/src/process_inbound_connection.rs
+++ b/src/rust/endpoint-plugin/src/process_inbound_connection.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplSessionId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplSessionId)]
 pub struct ProcessInboundConnection {
diff --git a/src/rust/endpoint-plugin/src/process_outbound_connection.rs b/src/rust/endpoint-plugin/src/process_outbound_connection.rs
index 075e6de5ac..60d506b524 100644
--- a/src/rust/endpoint-plugin/src/process_outbound_connection.rs
+++ b/src/rust/endpoint-plugin/src/process_outbound_connection.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplSessionId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplSessionId)]
 pub struct ProcessOutboundConnection {
diff --git a/src/rust/generators/generic-subgraph-generator/Cargo.toml b/src/rust/generators/generic-subgraph-generator/Cargo.toml
index 899d10a3bd..9bbb45e5f0 100644
--- a/src/rust/generators/generic-subgraph-generator/Cargo.toml
+++ b/src/rust/generators/generic-subgraph-generator/Cargo.toml
@@ -5,7 +5,7 @@ authors = ["Insanitybit <insanitybit@gmail.com>"]
 edition = "2018"
 
 [dependencies]
-grapl-graph-descriptions = { path = "../../graph-descriptions", version = "*" }
+rust-proto = { path = "../../rust-proto", version = "*" }
 endpoint-plugin = { path = "../../endpoint-plugin", version = "*" }
 grapl-config = { path = "../../grapl-config", version = "*" }
 sqs-executor = { path = "../../sqs-executor/" }
diff --git a/src/rust/generators/generic-subgraph-generator/src/generator.rs b/src/rust/generators/generic-subgraph-generator/src/generator.rs
index e6732857a5..2fa6e5cb37 100644
--- a/src/rust/generators/generic-subgraph-generator/src/generator.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/generator.rs
@@ -1,7 +1,7 @@
 use std::convert::TryFrom;
 
 use async_trait::async_trait;
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use sqs_executor::{
     cache::Cache,
     errors::{
diff --git a/src/rust/generators/generic-subgraph-generator/src/models/file/create.rs b/src/rust/generators/generic-subgraph-generator/src/models/file/create.rs
index 4d409b618a..ce6eb5a753 100644
--- a/src/rust/generators/generic-subgraph-generator/src/models/file/create.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/models/file/create.rs
@@ -8,7 +8,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/generic-subgraph-generator/src/models/file/delete.rs b/src/rust/generators/generic-subgraph-generator/src/models/file/delete.rs
index 094a45dd23..f1a2a0d437 100644
--- a/src/rust/generators/generic-subgraph-generator/src/models/file/delete.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/models/file/delete.rs
@@ -8,7 +8,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/generic-subgraph-generator/src/models/file/read.rs b/src/rust/generators/generic-subgraph-generator/src/models/file/read.rs
index b6e827592b..0bc3562d33 100644
--- a/src/rust/generators/generic-subgraph-generator/src/models/file/read.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/models/file/read.rs
@@ -8,7 +8,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/generic-subgraph-generator/src/models/file/write.rs b/src/rust/generators/generic-subgraph-generator/src/models/file/write.rs
index 9c22e0a9d6..d54d63ce3d 100644
--- a/src/rust/generators/generic-subgraph-generator/src/models/file/write.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/models/file/write.rs
@@ -8,7 +8,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/generic-subgraph-generator/src/models/mod.rs b/src/rust/generators/generic-subgraph-generator/src/models/mod.rs
index b9519ba002..bad47d4cee 100644
--- a/src/rust/generators/generic-subgraph-generator/src/models/mod.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/models/mod.rs
@@ -4,11 +4,11 @@ mod process;
 
 use std::convert::TryFrom;
 
-use grapl_graph_descriptions::graph_description::*;
 use process::{
     ProcessStart,
     ProcessStop,
 };
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/generic-subgraph-generator/src/models/network/inbound.rs b/src/rust/generators/generic-subgraph-generator/src/models/network/inbound.rs
index 5bb4fc71f0..6017f58059 100644
--- a/src/rust/generators/generic-subgraph-generator/src/models/network/inbound.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/models/network/inbound.rs
@@ -14,7 +14,7 @@ use endpoint_plugin::{
     ProcessInboundConnectionNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/generic-subgraph-generator/src/models/network/outbound.rs b/src/rust/generators/generic-subgraph-generator/src/models/network/outbound.rs
index 301ee54f39..fa3a5866ec 100644
--- a/src/rust/generators/generic-subgraph-generator/src/models/network/outbound.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/models/network/outbound.rs
@@ -14,7 +14,7 @@ use endpoint_plugin::{
     ProcessNode,
     ProcessOutboundConnectionNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/generic-subgraph-generator/src/models/process/start.rs b/src/rust/generators/generic-subgraph-generator/src/models/process/start.rs
index 5b573b491b..7bd3864b88 100644
--- a/src/rust/generators/generic-subgraph-generator/src/models/process/start.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/models/process/start.rs
@@ -8,7 +8,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/generic-subgraph-generator/src/models/process/stop.rs b/src/rust/generators/generic-subgraph-generator/src/models/process/stop.rs
index 657841bccf..4a80b1a4da 100644
--- a/src/rust/generators/generic-subgraph-generator/src/models/process/stop.rs
+++ b/src/rust/generators/generic-subgraph-generator/src/models/process/stop.rs
@@ -6,7 +6,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/graph-generator-lib/Cargo.toml b/src/rust/generators/graph-generator-lib/Cargo.toml
index f8bc475e4a..152e237764 100644
--- a/src/rust/generators/graph-generator-lib/Cargo.toml
+++ b/src/rust/generators/graph-generator-lib/Cargo.toml
@@ -7,7 +7,7 @@ description = "Library for writing Grapl generator services"
 license = "Apache-2.0"
 
 [dependencies]
-grapl-graph-descriptions = { path = "../../graph-descriptions", version = "0.2.*" }
+rust-proto = { path = "../../rust-proto", version = "0.2.*" }
 sqs-executor = { path = "../../sqs-executor" }
 grapl-service = { path = "../../grapl-service" }
 grapl-observe = { path = "../../grapl-observe" }
diff --git a/src/rust/generators/graph-generator-lib/src/lib.rs b/src/rust/generators/graph-generator-lib/src/lib.rs
index 72bcbd6f6c..c576f21ce5 100755
--- a/src/rust/generators/graph-generator-lib/src/lib.rs
+++ b/src/rust/generators/graph-generator-lib/src/lib.rs
@@ -7,11 +7,11 @@ use grapl_config::{
     event_caches,
     ServiceEnv,
 };
-pub use grapl_graph_descriptions::graph_description::*;
 pub use grapl_observe::metric_reporter::MetricReporter;
 use grapl_service::serialization::GraphDescriptionSerializer;
 use rusoto_s3::S3Client;
 use rusoto_sqs::SqsClient;
+pub use rust_proto::graph_descriptions::*;
 use sqs_executor::{
     errors::CheckedError,
     event_decoder::PayloadDecoder,
diff --git a/src/rust/generators/osquery-generator/Cargo.toml b/src/rust/generators/osquery-generator/Cargo.toml
index c3b66ec38b..aa12c69fbb 100644
--- a/src/rust/generators/osquery-generator/Cargo.toml
+++ b/src/rust/generators/osquery-generator/Cargo.toml
@@ -15,7 +15,7 @@ name = "generator_bench"
 harness = false
 
 [dependencies]
-grapl-graph-descriptions = { path = "../../graph-descriptions", version = "*" }
+rust-proto = { path = "../../rust-proto", version = "*" }
 endpoint-plugin = { path = "../../endpoint-plugin" }
 grapl-config = { path = "../../grapl-config", version = "*" }
 grapl-observe = { path = "../../grapl-observe", version = "*" }
diff --git a/src/rust/generators/osquery-generator/src/generator.rs b/src/rust/generators/osquery-generator/src/generator.rs
index 7e05212e70..82cff656c4 100644
--- a/src/rust/generators/osquery-generator/src/generator.rs
+++ b/src/rust/generators/osquery-generator/src/generator.rs
@@ -1,5 +1,5 @@
 use async_trait::async_trait;
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use sqs_executor::{
     cache::Cache,
     errors::{
diff --git a/src/rust/generators/osquery-generator/src/parsers/grapl_pack/files.rs b/src/rust/generators/osquery-generator/src/parsers/grapl_pack/files.rs
index 03c9033ed3..ee578528e4 100644
--- a/src/rust/generators/osquery-generator/src/parsers/grapl_pack/files.rs
+++ b/src/rust/generators/osquery-generator/src/parsers/grapl_pack/files.rs
@@ -4,7 +4,7 @@ use endpoint_plugin::{
     IAssetNode,
     IFileNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/osquery-generator/src/parsers/grapl_pack/process_files.rs b/src/rust/generators/osquery-generator/src/parsers/grapl_pack/process_files.rs
index 7e492e1b26..3b7fbcfef6 100644
--- a/src/rust/generators/osquery-generator/src/parsers/grapl_pack/process_files.rs
+++ b/src/rust/generators/osquery-generator/src/parsers/grapl_pack/process_files.rs
@@ -6,7 +6,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/osquery-generator/src/parsers/grapl_pack/processes.rs b/src/rust/generators/osquery-generator/src/parsers/grapl_pack/processes.rs
index 515d88c101..09643d2132 100644
--- a/src/rust/generators/osquery-generator/src/parsers/grapl_pack/processes.rs
+++ b/src/rust/generators/osquery-generator/src/parsers/grapl_pack/processes.rs
@@ -6,7 +6,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/osquery-generator/src/parsers/mod.rs b/src/rust/generators/osquery-generator/src/parsers/mod.rs
index 7baa75ed4c..b99ed53fe7 100644
--- a/src/rust/generators/osquery-generator/src/parsers/mod.rs
+++ b/src/rust/generators/osquery-generator/src/parsers/mod.rs
@@ -1,4 +1,4 @@
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/generators/sysmon-generator/Cargo.toml b/src/rust/generators/sysmon-generator/Cargo.toml
index f84c7f5ece..d53d715578 100644
--- a/src/rust/generators/sysmon-generator/Cargo.toml
+++ b/src/rust/generators/sysmon-generator/Cargo.toml
@@ -15,7 +15,7 @@ name = "generator_bench"
 harness = false
 
 [dependencies]
-grapl-graph-descriptions = { path = "../../graph-descriptions", version = "*" }
+rust-proto = { path = "../../rust-proto", version = "*" }
 endpoint-plugin = { path = "../../endpoint-plugin", version = "*" }
 grapl-config = { path = "../../grapl-config", version = "*" }
 grapl-observe = { path = "../../grapl-observe", version = "*" }
diff --git a/src/rust/generators/sysmon-generator/src/generator.rs b/src/rust/generators/sysmon-generator/src/generator.rs
index bbbf553255..d11c5e44c8 100644
--- a/src/rust/generators/sysmon-generator/src/generator.rs
+++ b/src/rust/generators/sysmon-generator/src/generator.rs
@@ -1,5 +1,5 @@
 use async_trait::async_trait;
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use sqs_executor::{
     cache::Cache,
     errors::{
diff --git a/src/rust/generators/sysmon-generator/src/models/file/create.rs b/src/rust/generators/sysmon-generator/src/models/file/create.rs
index 6aff02bc63..adeade5e57 100644
--- a/src/rust/generators/sysmon-generator/src/models/file/create.rs
+++ b/src/rust/generators/sysmon-generator/src/models/file/create.rs
@@ -6,7 +6,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use sysmon::FileCreateEvent;
 
 use crate::{
diff --git a/src/rust/generators/sysmon-generator/src/models/mod.rs b/src/rust/generators/sysmon-generator/src/models/mod.rs
index d189416770..9df4bb6d9d 100644
--- a/src/rust/generators/sysmon-generator/src/models/mod.rs
+++ b/src/rust/generators/sysmon-generator/src/models/mod.rs
@@ -3,7 +3,7 @@ use chrono::{
     NaiveDateTime,
     Utc,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use sysmon::Event;
 
 use crate::generator::SysmonGeneratorError;
@@ -12,7 +12,7 @@ mod file;
 mod network;
 mod process;
 
-/// Because this crate doesn't own sysmon::Event nor grapl_graph_descriptions::graph_description::GraphDescription
+/// Because this crate doesn't own sysmon::Event nor rust_proto::graph_descriptions::GraphDescription
 /// we need to create a new Trait to add a function to graph for Event.
 ///
 /// Mimics TryFrom
diff --git a/src/rust/generators/sysmon-generator/src/models/network/inbound.rs b/src/rust/generators/sysmon-generator/src/models/network/inbound.rs
index 2a5b144b98..b632687f53 100644
--- a/src/rust/generators/sysmon-generator/src/models/network/inbound.rs
+++ b/src/rust/generators/sysmon-generator/src/models/network/inbound.rs
@@ -1,4 +1,4 @@
-use grapl_graph_descriptions::{graph_description::*,
+use rust_proto::{graph_descriptions::*,
                                network_connection::NetworkConnectionState,
                                node::NodeT,
                                process::ProcessState,
diff --git a/src/rust/generators/sysmon-generator/src/models/network/outbound.rs b/src/rust/generators/sysmon-generator/src/models/network/outbound.rs
index ad0a0e07ae..8ac63cb763 100644
--- a/src/rust/generators/sysmon-generator/src/models/network/outbound.rs
+++ b/src/rust/generators/sysmon-generator/src/models/network/outbound.rs
@@ -14,7 +14,7 @@ use endpoint_plugin::{
     ProcessNode,
     ProcessOutboundConnectionNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use sysmon::NetworkEvent;
 
 use crate::{
diff --git a/src/rust/generators/sysmon-generator/src/models/process/create.rs b/src/rust/generators/sysmon-generator/src/models/process/create.rs
index 6919cbb8fc..500bff07c9 100644
--- a/src/rust/generators/sysmon-generator/src/models/process/create.rs
+++ b/src/rust/generators/sysmon-generator/src/models/process/create.rs
@@ -6,7 +6,7 @@ use endpoint_plugin::{
     IProcessNode,
     ProcessNode,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 use sysmon::ProcessCreateEvent;
 
 use crate::{
@@ -95,8 +95,8 @@ pub fn generate_process_create_subgraph(
 mod tests {
     use std::str::FromStr;
 
-    use grapl_graph_descriptions::{
-        graph_description::node_property::Property,
+    use rust_proto::graph_descriptions::{
+        node_property::Property,
         ImmutableUintProp,
     };
     use sysmon::*;
diff --git a/src/rust/generators/sysmon-generator/src/serialization.rs b/src/rust/generators/sysmon-generator/src/serialization.rs
index 8b4d576219..60f67a56fb 100644
--- a/src/rust/generators/sysmon-generator/src/serialization.rs
+++ b/src/rust/generators/sysmon-generator/src/serialization.rs
@@ -55,7 +55,10 @@ impl PayloadDecoder<Vec<Event>> for SysmonDecoder {
             .filter_map(|event_str| {
                 let parsed_event = Event::from_str(&event_str);
                 match parsed_event {
-                    Ok(event) => Some(event),
+                    Ok(event) => {
+                        tracing::debug!(message = "Deserialized sysmon event");
+                        Some(event)
+                    }
                     Err(error) => {
                         tracing::error!(
                             message = "Unable to deserialize Sysmon event",
diff --git a/src/rust/graph-merger/Cargo.toml b/src/rust/graph-merger/Cargo.toml
index be423ae587..1c6d7117a7 100644
--- a/src/rust/graph-merger/Cargo.toml
+++ b/src/rust/graph-merger/Cargo.toml
@@ -11,7 +11,7 @@ name = "graph-merger"
 name = "graph_merger_lib"
 
 [dependencies]
-grapl-graph-descriptions = { path = "../graph-descriptions" }
+rust-proto = { path = "../rust-proto" }
 grapl-config = { path = "../grapl-config", version = "*" }
 grapl-observe = { path = "../grapl-observe", version = "*" }
 grapl-service = { path = "../grapl-service" }
diff --git a/src/rust/graph-merger/src/main.rs b/src/rust/graph-merger/src/main.rs
index dae521010a..792839a89e 100755
--- a/src/rust/graph-merger/src/main.rs
+++ b/src/rust/graph-merger/src/main.rs
@@ -39,14 +39,6 @@ use grapl_config::{
     },
     event_caches,
 };
-use grapl_graph_descriptions::graph_description::{
-    Edge,
-    EdgeList,
-    IdentifiedGraph,
-    IdentifiedNode,
-    MergedGraph,
-    MergedNode,
-};
 use grapl_observe::{
     dgraph_reporter::DgraphMetricReporter,
     metric_reporter::{
@@ -73,6 +65,14 @@ use rusoto_dynamodb::{
 };
 use rusoto_s3::S3Client;
 use rusoto_sqs::SqsClient;
+use rust_proto::graph_descriptions::{
+    Edge,
+    EdgeList,
+    IdentifiedGraph,
+    IdentifiedNode,
+    MergedGraph,
+    MergedNode,
+};
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/graph-merger/src/reverse_resolver.rs b/src/rust/graph-merger/src/reverse_resolver.rs
index 1bef9ed30b..c2515739df 100644
--- a/src/rust/graph-merger/src/reverse_resolver.rs
+++ b/src/rust/graph-merger/src/reverse_resolver.rs
@@ -3,7 +3,6 @@ use std::{
     io::Stdout,
 };
 
-use grapl_graph_descriptions::Edge;
 use grapl_observe::metric_reporter::MetricReporter;
 use grapl_utils::{
     future_ext::GraplFutureExt,
@@ -16,6 +15,7 @@ use rusoto_dynamodb::{
     DynamoDbClient,
     KeysAndAttributes,
 };
+use rust_proto::graph_descriptions::Edge;
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/graph-merger/src/service.rs b/src/rust/graph-merger/src/service.rs
index b8fc8d50cd..d88427dd5a 100644
--- a/src/rust/graph-merger/src/service.rs
+++ b/src/rust/graph-merger/src/service.rs
@@ -30,14 +30,6 @@ use grapl_config::{
     },
     event_caches,
 };
-use grapl_graph_descriptions::graph_description::{
-    Edge,
-    EdgeList,
-    IdentifiedGraph,
-    IdentifiedNode,
-    MergedGraph,
-    MergedNode,
-};
 use grapl_observe::{
     dgraph_reporter::DgraphMetricReporter,
     metric_reporter::{
@@ -64,6 +56,14 @@ use rusoto_dynamodb::{
 };
 use rusoto_s3::S3Client;
 use rusoto_sqs::SqsClient;
+use rust_proto::graph_descriptions::{
+    Edge,
+    EdgeList,
+    IdentifiedGraph,
+    IdentifiedNode,
+    MergedGraph,
+    MergedNode,
+};
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/graph-merger/src/upsert_util.rs b/src/rust/graph-merger/src/upsert_util.rs
index 19a4792633..afd1402ba9 100644
--- a/src/rust/graph-merger/src/upsert_util.rs
+++ b/src/rust/graph-merger/src/upsert_util.rs
@@ -1,9 +1,5 @@
 use std::collections::HashMap;
 
-use grapl_graph_descriptions::{
-    graph_description::*,
-    node_property::Property,
-};
 use node_property::Property::{
     DecrementOnlyInt as ProtoDecrementOnlyIntProp,
     DecrementOnlyUint as ProtoDecrementOnlyUintProp,
@@ -13,6 +9,10 @@ use node_property::Property::{
     IncrementOnlyInt as ProtoIncrementOnlyIntProp,
     IncrementOnlyUint as ProtoIncrementOnlyUintProp,
 };
+use rust_proto::{
+    graph_descriptions::*,
+    node_property::Property,
+};
 
 pub struct Escaped(String);
 
diff --git a/src/rust/graph-merger/src/upserter.rs b/src/rust/graph-merger/src/upserter.rs
index 901c89cd50..67ed5f1c1e 100644
--- a/src/rust/graph-merger/src/upserter.rs
+++ b/src/rust/graph-merger/src/upserter.rs
@@ -43,17 +43,8 @@ use futures_retry::{
     FutureRetry,
     RetryPolicy,
 };
-use grapl_graph_descriptions::{
-    graph_description::*,
-    node_property::Property,
-    MergedGraph,
-};
 use grapl_utils::iter_ext::GraplIterExt;
-// use grapl_graph_descriptions::Edge;
-// use grapl_graph_descriptions::EdgeList;
-// use grapl_graph_descriptions::MergedNode;
-// use grapl_graph_descriptions::IdentifiedNode;
-pub use node_property::Property::{
+pub use rust_proto::node_property::Property::{
     DecrementOnlyInt as ProtoDecrementOnlyIntProp,
     DecrementOnlyUint as ProtoDecrementOnlyUintProp,
     ImmutableInt as ProtoImmutableIntProp,
@@ -62,6 +53,10 @@ pub use node_property::Property::{
     IncrementOnlyInt as ProtoIncrementOnlyIntProp,
     IncrementOnlyUint as ProtoIncrementOnlyUintProp,
 };
+use rust_proto::{
+    graph_descriptions::*,
+    node_property::Property,
+};
 
 use crate::upsert_util;
 
diff --git a/src/rust/grapl-service/Cargo.toml b/src/rust/grapl-service/Cargo.toml
index 4dd498e24a..24106e7c00 100644
--- a/src/rust/grapl-service/Cargo.toml
+++ b/src/rust/grapl-service/Cargo.toml
@@ -7,7 +7,7 @@ edition = "2018"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [dependencies]
 sqs-executor = { path = "../sqs-executor" }
-grapl-graph-descriptions = { path = "../graph-descriptions" }
+rust-proto = { path = "../rust-proto" }
 grapl-config = { path = "../grapl-config" }
 zstd = "0.7"
 libflate = "1"
diff --git a/src/rust/grapl-service/src/serialization/zstd_proto_graph.rs b/src/rust/grapl-service/src/serialization/zstd_proto_graph.rs
index af67fe825c..07018f92bc 100644
--- a/src/rust/grapl-service/src/serialization/zstd_proto_graph.rs
+++ b/src/rust/grapl-service/src/serialization/zstd_proto_graph.rs
@@ -1,5 +1,5 @@
-use grapl_graph_descriptions::graph_description::*;
 use prost::EncodeError;
+use rust_proto::graph_descriptions::*;
 use sqs_executor::completion_event_serializer::CompletionEventSerializer;
 use tracing::{
     debug,
diff --git a/src/rust/grapl/Cargo.toml b/src/rust/grapl/Cargo.toml
index 392f266a75..26e05aa63d 100644
--- a/src/rust/grapl/Cargo.toml
+++ b/src/rust/grapl/Cargo.toml
@@ -10,4 +10,4 @@ license = "Apache-2.0"
 derive-dynamic-node = { path = "../derive-dynamic-node" }
 graph-generator-lib = { path = "../generators/graph-generator-lib/" }
 grapl-config = { path = "../grapl-config" }
-grapl-graph-descriptions = { path = "../graph-descriptions" }
+rust-proto = { path = "../rust-proto" }
diff --git a/src/rust/grapl/src/lib.rs b/src/rust/grapl/src/lib.rs
index 53cd5ce7f6..30b8686c61 100644
--- a/src/rust/grapl/src/lib.rs
+++ b/src/rust/grapl/src/lib.rs
@@ -1,4 +1,4 @@
 pub use derive_dynamic_node;
 pub use graph_generator_lib as generator_lib;
 pub use grapl_config as config;
-pub use grapl_graph_descriptions as graph_descriptions;
+pub use rust_proto as graph_descriptions;
diff --git a/src/rust/node-identifier/Cargo.toml b/src/rust/node-identifier/Cargo.toml
index 530ae46007..c259b62220 100644
--- a/src/rust/node-identifier/Cargo.toml
+++ b/src/rust/node-identifier/Cargo.toml
@@ -16,7 +16,7 @@ path = "src/bin/node-identifier-retry.rs"
 integration = []
 
 [dependencies]
-grapl-graph-descriptions = { path = "../graph-descriptions", version = "*" }
+rust-proto = { path = "../rust-proto", version = "*" }
 grapl-config = { path = "../grapl-config", version = "*" }
 grapl-observe = { path = "../grapl-observe", version = "*" }
 grapl-service = { path = "../grapl-service" }
@@ -66,3 +66,6 @@ tracing = "0.1.26"
 [dev-dependencies]
 quickcheck = "1"
 quickcheck_macros = "1"
+
+[build-dependencies]
+prost-build = "0.7.0"
diff --git a/src/rust/node-identifier/src/dynamic_sessiondb.rs b/src/rust/node-identifier/src/dynamic_sessiondb.rs
index 22091ca092..2e3bd3517d 100644
--- a/src/rust/node-identifier/src/dynamic_sessiondb.rs
+++ b/src/rust/node-identifier/src/dynamic_sessiondb.rs
@@ -7,17 +7,17 @@ use failure::{
     bail,
     Error,
 };
-use grapl_graph_descriptions::graph_description::{
-    id_strategy,
-    Session as SessionStrategy,
-    *,
-};
 use rusoto_dynamodb::{
     AttributeValue,
     DynamoDb,
     GetItemInput,
     PutItemInput,
 };
+use rust_proto::graph_descriptions::{
+    id_strategy,
+    Session as SessionStrategy,
+    *,
+};
 use serde::{
     Deserialize,
     Serialize,
diff --git a/src/rust/node-identifier/src/lib.rs b/src/rust/node-identifier/src/lib.rs
index b6a3513fb3..acd04bde0a 100755
--- a/src/rust/node-identifier/src/lib.rs
+++ b/src/rust/node-identifier/src/lib.rs
@@ -1,5 +1,3 @@
-// #![allow(unused_must_use)]
-
 use std::collections::HashMap;
 
 use async_trait::async_trait;
@@ -15,12 +13,6 @@ use grapl_config::{
     },
     event_caches,
 };
-use grapl_graph_descriptions::graph_description::{
-    GraphDescription,
-    IdentifiedGraph,
-    IdentifiedNode,
-    NodeDescription,
-};
 use grapl_observe::metric_reporter::MetricReporter;
 use grapl_service::{
     decoder::ProtoDecoder,
@@ -29,6 +21,12 @@ use grapl_service::{
 use grapl_utils::rusoto_ext::dynamodb::GraplDynamoDbClientExt;
 use rusoto_dynamodb::DynamoDbClient;
 use rusoto_sqs::SqsClient;
+use rust_proto::graph_descriptions::{
+    GraphDescription,
+    IdentifiedGraph,
+    IdentifiedNode,
+    NodeDescription,
+};
 use sessiondb::SessionDb;
 use sqs_executor::{
     cache::Cache,
@@ -59,8 +57,8 @@ pub mod sessions;
 
     The strategies come in two variants:
 
-    * [Session](`grapl_graph_descriptions::graph_description::Session`) - strategy used for nodes with lifetimes (e.g. process, network connection)
-    * [Static](`grapl_graph_descriptions::graph_description::Static`) - strategy used for nodes with canonical and unique identifiers (e.g. aws events)
+    * [Session](`rust_proto::graph_descriptions::Session`) - strategy used for nodes with lifetimes (e.g. process, network connection)
+    * [Static](`rust_proto::graph_descriptions::Static`) - strategy used for nodes with canonical and unique identifiers (e.g. aws events)
 */
 #[derive(Clone)]
 pub struct NodeIdentifier<D, CacheT>
@@ -210,7 +208,7 @@ where
     #[tracing::instrument(skip(self, unidentified_subgraph, _completed))]
     async fn handle_event(
         &mut self,
-        unidentified_subgraph: GraphDescription,
+        unidentified_subgraph: Self::InputEvent,
         _completed: &mut CompletedEvents,
     ) -> Result<Self::OutputEvent, Result<(Self::OutputEvent, Self::Error), Self::Error>> {
         /*
diff --git a/src/rust/graph-descriptions/Cargo.toml b/src/rust/rust-proto/Cargo.toml
similarity index 88%
rename from src/rust/graph-descriptions/Cargo.toml
rename to src/rust/rust-proto/Cargo.toml
index f4dabbfe3e..da37273e72 100644
--- a/src/rust/graph-descriptions/Cargo.toml
+++ b/src/rust/rust-proto/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "grapl-graph-descriptions"
+name = "rust-proto"
 version = "0.2.10"
 authors = ["Insanitybit <insanitybit@gmail.com>"]
 edition = "2018"
@@ -15,6 +15,7 @@ rand = "0.8"
 serde = "1"
 serde_derive = "1"
 tracing = "0.1.22"
+uuid = { version = "0.8.2", features = ["v4"] }
 
 [build-dependencies]
 prost-build = "0.7"
diff --git a/src/rust/graph-descriptions/build.rs b/src/rust/rust-proto/build.rs
similarity index 94%
rename from src/rust/graph-descriptions/build.rs
rename to src/rust/rust-proto/build.rs
index 447bd97afb..a8c7ec6602 100644
--- a/src/rust/graph-descriptions/build.rs
+++ b/src/rust/rust-proto/build.rs
@@ -1,6 +1,11 @@
-fn main() {
+fn main() -> Result<(), Box<dyn std::error::Error>> {
     let mut config = prost_build::Config::new();
+    println!("cargo:rerun-if-changed=Cargo.toml");
+    println!("cargo:rerun-if-changed=Cargo.lock");
+    println!("cargo:rerun-if-changed=build.rs");
 
+    change_on_dir("../../proto/")?;
+    change_on_dir("src/")?;
     config.type_attribute(
         ".",
         "#[derive(Eq, serde_derive::Serialize, serde_derive::Deserialize)]",
@@ -457,8 +462,25 @@ fn main() {
 
     config
         .compile_protos(
-            &["../../proto/graplinc/grapl/api/graph/v1beta1/types.proto"],
+            &[
+                "../../proto/graplinc/grapl/api/graph/v1beta1/types.proto",
+                "../../proto/graplinc/grapl/pipeline/v1beta1/types.proto",
+            ],
             &["../../proto/"],
         )
         .unwrap_or_else(|e| panic!("protobuf compilation failed: {}", e));
+    Ok(())
+}
+
+fn change_on_dir(root_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
+    let current_dir = std::env::current_dir()?;
+    for entry in std::fs::read_dir(current_dir.join(root_dir))? {
+        let entry = entry?;
+        if !entry.metadata()?.is_file() {
+            continue;
+        }
+        let path = entry.path();
+        println!("cargo:rerun-if-changed={}", path.display());
+    }
+    Ok(())
 }
diff --git a/src/rust/graph-descriptions/src/lib.rs b/src/rust/rust-proto/src/graph_descriptions.rs
similarity index 98%
rename from src/rust/graph-descriptions/src/lib.rs
rename to src/rust/rust-proto/src/graph_descriptions.rs
index fe630dc56a..a1ab43ae18 100644
--- a/src/rust/graph-descriptions/src/lib.rs
+++ b/src/rust/rust-proto/src/graph_descriptions.rs
@@ -1,12 +1,5 @@
-pub mod graph_description {
-    // TODO: Restructure the Rust modules to better reflect the new
-    // Protobuf structure
-    include!(concat!(
-        env!("OUT_DIR"),
-        "/graplinc.grapl.api.graph.v1beta1.rs"
-    ));
-}
-pub use node_property::Property::{
+pub use node_property::Property;
+use node_property::Property::{
     DecrementOnlyInt as ProtoDecrementOnlyIntProp,
     DecrementOnlyUint as ProtoDecrementOnlyUintProp,
     ImmutableInt as ProtoImmutableIntProp,
@@ -16,10 +9,8 @@ pub use node_property::Property::{
     IncrementOnlyUint as ProtoIncrementOnlyUintProp,
 };
 
-pub use crate::{
-    graph_description::*,
-    node_property::Property,
-};
+pub use crate::graplinc::grapl::api::graph::v1beta1::*;
+use crate::pipeline::ServiceMessage;
 
 // A helper macro to generate `From` impl boilerplate.
 macro_rules ! impl_from_for_unit {
@@ -58,6 +49,10 @@ impl EdgeList {
     }
 }
 
+impl ServiceMessage for GraphDescription {
+    const TYPE_NAME: &'static str = "graplinc.grapl.api.graph.v1beta1.GraphDescription";
+}
+
 impl GraphDescription {
     pub fn new() -> Self {
         Self {
@@ -200,6 +195,10 @@ impl IdentifiedGraph {
     }
 }
 
+impl ServiceMessage for IdentifiedGraph {
+    const TYPE_NAME: &'static str = "graplinc.grapl.api.graph.v1beta1.IdentifiedGraph";
+}
+
 impl MergedGraph {
     pub fn new() -> Self {
         Self {
@@ -313,6 +312,10 @@ impl NodeDescription {
     }
 }
 
+impl ServiceMessage for MergedGraph {
+    const TYPE_NAME: &'static str = "graplinc.grapl.api.graph.v1beta1.MergedGraph";
+}
+
 impl IdentifiedNode {
     pub fn merge(&mut self, other: &Self) {
         extra_assert!(debug_assert_eq!(self.node_type, other.node_type));
diff --git a/src/rust/rust-proto/src/lib.rs b/src/rust/rust-proto/src/lib.rs
new file mode 100644
index 0000000000..cf8e6a3460
--- /dev/null
+++ b/src/rust/rust-proto/src/lib.rs
@@ -0,0 +1,27 @@
+pub(crate) mod graplinc {
+    pub(crate) mod grapl {
+        pub(crate) mod api {
+            pub(crate) mod graph {
+                pub(crate) mod v1beta1 {
+                    include!(concat!(
+                        env!("OUT_DIR"),
+                        "/graplinc.grapl.api.graph.v1beta1.rs"
+                    ));
+                }
+            }
+        }
+        pub(crate) mod pipeline {
+            pub mod v1beta1 {
+                include!(concat!(
+                    env!("OUT_DIR"),
+                    "/graplinc.grapl.pipeline.v1beta1.rs"
+                ));
+            }
+        }
+    }
+}
+
+pub mod graph_descriptions;
+pub use graph_descriptions::node_property;
+
+pub mod pipeline;
diff --git a/src/rust/rust-proto/src/pipeline.rs b/src/rust/rust-proto/src/pipeline.rs
new file mode 100644
index 0000000000..c453eaf02e
--- /dev/null
+++ b/src/rust/rust-proto/src/pipeline.rs
@@ -0,0 +1,80 @@
+pub use grapl_pipeline::{
+    Envelope,
+    Metadata,
+    Uuid,
+};
+use uuid;
+
+pub use crate::graplinc::grapl::pipeline::v1beta1 as grapl_pipeline;
+
+pub trait ServiceMessage {
+    const TYPE_NAME: &'static str;
+}
+
+impl grapl_pipeline::Uuid {
+    pub fn new() -> Self {
+        Self::from(uuid::Uuid::new_v4())
+    }
+}
+
+impl From<grapl_pipeline::Uuid> for uuid::Uuid {
+    fn from(p: grapl_pipeline::Uuid) -> Self {
+        let lsb: [u8; 8] = p.lsb.to_le_bytes();
+        let msb: [u8; 8] = p.msb.to_le_bytes();
+        let u: u128 = u128::from_le_bytes([
+            lsb[0], lsb[1], lsb[2], lsb[3], lsb[4], lsb[5], lsb[6], lsb[7], msb[0], msb[1], msb[2],
+            msb[3], msb[4], msb[5], msb[6], msb[7],
+        ]);
+
+        uuid::Builder::from_u128(u).build()
+    }
+}
+
+impl From<uuid::Uuid> for grapl_pipeline::Uuid {
+    fn from(u: uuid::Uuid) -> Self {
+        let u = u.as_u128();
+        let u_le: [u8; 16] = u.to_le_bytes();
+        // Ugly, but `Index` isn't const, so Rust can't figure
+        // out that half of a [u8; 16] is two [u8; 8]'s
+        let lsb: u64 = u64::from_le_bytes([
+            u_le[0], u_le[1], u_le[2], u_le[3], u_le[4], u_le[5], u_le[6], u_le[7],
+        ]);
+        let msb: u64 = u64::from_le_bytes([
+            u_le[8], u_le[9], u_le[10], u_le[11], u_le[12], u_le[13], u_le[14], u_le[15],
+        ]);
+        Self { lsb, msb }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_equality_from_uuid() {
+        for _ in 0..1000 {
+            // start with a uuid::Uuid
+            let u0 = uuid::Uuid::new_v4();
+            // Convert it into a protobuf Uuid
+            let pu: grapl_pipeline::Uuid = u0.clone().into();
+            // Then back to a uuid::Uuid
+            let u1: uuid::Uuid = pu.into();
+            // Hopefully it hasn't changed
+            assert_eq!(u0, u1);
+        }
+    }
+
+    #[test]
+    fn test_equality_from_proto_uuid() {
+        for _ in 0..1000 {
+            // start with a protobuf Uuid
+            let pu0 = grapl_pipeline::Uuid::new();
+            // Convert it into a uuid::Uuid
+            let u: uuid::Uuid = pu0.clone().into();
+            // Then back to a protobuf Uuid
+            let pu1: grapl_pipeline::Uuid = u.into();
+            // Hopefully it hasn't changed
+            assert_eq!(pu0, pu1);
+        }
+    }
+}
diff --git a/src/rust/sqs-executor/Cargo.toml b/src/rust/sqs-executor/Cargo.toml
index 6f48cdbcb4..5aa9f0c2d3 100644
--- a/src/rust/sqs-executor/Cargo.toml
+++ b/src/rust/sqs-executor/Cargo.toml
@@ -11,6 +11,7 @@ integration = []
 [dependencies]
 grapl-observe = { path = "../grapl-observe" }
 grapl-utils = { path = "../grapl-utils" }
+rust-proto = { path = "../rust-proto/" }
 rusoto_core = { version = "0.46", default_features = false, features = [
   "rustls"
 ] }
diff --git a/src/rust/sqs-executor/src/lib.rs b/src/rust/sqs-executor/src/lib.rs
index ef65ba9f58..0414f31e2a 100644
--- a/src/rust/sqs-executor/src/lib.rs
+++ b/src/rust/sqs-executor/src/lib.rs
@@ -1,5 +1,3 @@
-pub mod retriever;
-
 use std::{
     error::Error,
     fmt::Debug,
@@ -23,6 +21,12 @@ use grapl_observe::{
     },
     timers::TimedFutureExt,
 };
+use prost::Message;
+pub use retriever::{
+    event_retriever,
+    s3_event_retriever,
+};
+use rusoto_core::RusotoError;
 use rusoto_s3::S3;
 use rusoto_sqs::{
     ListQueuesError,
@@ -30,6 +34,7 @@ use rusoto_sqs::{
     Message as SqsMessage,
     Sqs,
 };
+use rust_proto::pipeline::ServiceMessage;
 use s3_event_emitter::S3EventEmitter;
 use s3_event_retriever::S3PayloadRetriever;
 use tracing::{
@@ -49,22 +54,17 @@ use crate::{
     event_handler::CompletedEvents,
     event_retriever::PayloadRetriever,
     event_status::EventStatus,
+    sqs_timeout_manager::keep_alive,
 };
 
+pub mod retriever;
+
 pub mod cache;
 pub mod completion_event_serializer;
 pub mod errors;
 pub mod event_decoder;
 pub mod event_emitter;
 pub mod event_handler;
-pub use retriever::{
-    event_retriever,
-    s3_event_retriever,
-};
-use rusoto_core::RusotoError;
-
-use crate::sqs_timeout_manager::keep_alive;
-
 pub mod event_status;
 pub mod key_creator;
 pub mod redis_cache;
@@ -169,7 +169,7 @@ async fn process_message<
     InputEventT: Send,
     EventHandlerT:
         EventHandler<InputEvent = InputEventT, OutputEvent = OutputEventT, Error = HandlerErrorT>,
-    OutputEventT: Clone + Send + Sync + 'static,
+    OutputEventT: ServiceMessage + Clone + Send + Sync + 'static,
     HandlerErrorT: CheckedError + Debug + Send + Sync + 'static,
     SerializerErrorT: Error + Debug + Send + Sync + 'static,
     S3ClientT: S3 + Clone + Send + Sync + 'static,
@@ -181,7 +181,11 @@ async fn process_message<
     >,
 {
     let message_id = next_message.message_id.as_ref().unwrap().as_str();
-    let inner_loop_span = tracing::trace_span!("inner_loop_span", message_id = message_id,);
+    let inner_loop_span = tracing::trace_span!(
+        "inner_loop_span",
+        message_id = message_id,
+        trace_id = tracing::field::Empty
+    );
     let _enter = inner_loop_span.enter();
 
     if cache.all_exist(&[message_id.to_owned()]).await {
@@ -215,8 +219,8 @@ async fn process_message<
     );
     let payload = s3_payload_retriever.retrieve_event(&next_message).await;
 
-    let events = match payload {
-        Ok(Some(events)) => events,
+    let (meta, events) = match payload {
+        Ok(Some((meta, events))) => (meta, events),
         Ok(None) => {
             rusoto_helpers::delete_message(
                 sqs_client.clone(),
@@ -249,6 +253,10 @@ async fn process_message<
         }
     };
 
+    let trace_id: uuid::Uuid = meta.trace_id.clone().unwrap().into();
+    let trace_id = trace_id.to_string();
+    inner_loop_span.record("trace_id", &trace_id.as_str());
+
     // todo: We can lift this
     let mut completed = CompletedEvents::default();
 
@@ -273,13 +281,34 @@ async fn process_message<
     match processing_result {
         Ok(total) => {
             // encode event
-            let event = serializer
+            let events = serializer
                 .serialize_completed_events(&[total])
                 .expect("Serializing failed");
+
+            if events.is_empty() {
+                tracing::debug!(message = "Serialized events produced no output");
+                return;
+            }
+
+            let mut envelopes = vec![];
+            for event in events {
+                let envelope = rust_proto::pipeline::Envelope {
+                    metadata: Some(meta.clone()),
+                    inner_type: OutputEventT::TYPE_NAME.to_string(),
+                    inner_message: event,
+                };
+                let mut encoded = vec![];
+                if let Err(e) = envelope.encode(&mut encoded) {
+                    tracing::error!(message="Failed to encode message", error=?e);
+                    continue;
+                };
+                envelopes.push(encoded);
+            }
+
             // emit event
             // todo: we should retry event emission
             s3_emitter
-                .emit_event(event)
+                .emit_event(envelopes)
                 .await
                 .expect("Failed to emit event");
 
@@ -306,16 +335,37 @@ async fn process_message<
                 error=?e,
                 recoverable=?e.error_type()
             );
-            let event = serializer
+            let events = serializer
                 .serialize_completed_events(&[partial])
                 .expect("Serializing failed");
+            // emit event
+            // todo: we should retry event emission
+            if events.is_empty() {
+                tracing::debug!(message = "Serialized events produced no output");
+                return;
+            }
+
+            let mut envelopes = vec![];
+            for event in events {
+                let envelope = rust_proto::pipeline::Envelope {
+                    metadata: Some(meta.clone()),
+                    inner_type: OutputEventT::TYPE_NAME.to_string(),
+                    inner_message: event,
+                };
+                let mut encoded = vec![];
+                if let Err(e) = envelope.encode(&mut encoded) {
+                    tracing::error!(message="Failed to encode message", error=?e);
+                    continue;
+                };
+                envelopes.push(encoded);
+            }
+
             // emit event
             // todo: we should retry event emission
             s3_emitter
-                .emit_event(event)
+                .emit_event(envelopes)
                 .await
                 .expect("Failed to emit event");
-
             cache_completed(cache, &mut completed).await;
 
             if let Recoverable::Persistent = e.error_type() {
@@ -391,7 +441,7 @@ async fn _process_loop<
     InputEventT: Send,
     EventHandlerT:
         EventHandler<InputEvent = InputEventT, OutputEvent = OutputEventT, Error = HandlerErrorT>,
-    OutputEventT: Clone + Send + Sync + 'static,
+    OutputEventT: ServiceMessage + Clone + Send + Sync + 'static,
     HandlerErrorT: CheckedError + Debug + Send + Sync + 'static,
     SerializerErrorT: Error + Debug + Send + Sync + 'static,
     S3ClientT: S3 + Clone + Send + Sync + 'static,
@@ -531,7 +581,7 @@ pub async fn process_loop<
     InputEventT: Send,
     EventHandlerT:
         EventHandler<InputEvent = InputEventT, OutputEvent = OutputEventT, Error = HandlerErrorT>,
-    OutputEventT: Clone + Send + Sync + 'static,
+    OutputEventT: ServiceMessage + Clone + Send + Sync + 'static,
     HandlerErrorT: CheckedError + Debug + Send + Sync + 'static,
     SerializerErrorT: Error + Debug + Send + Sync + 'static,
     S3ClientT: S3 + Clone + Send + Sync + 'static,
diff --git a/src/rust/sqs-executor/src/retriever/event_retriever.rs b/src/rust/sqs-executor/src/retriever/event_retriever.rs
index d82e341ee3..5d15e641ef 100644
--- a/src/rust/sqs-executor/src/retriever/event_retriever.rs
+++ b/src/rust/sqs-executor/src/retriever/event_retriever.rs
@@ -1,4 +1,5 @@
 use async_trait::async_trait;
+use rust_proto::pipeline::Metadata;
 
 use crate::errors::CheckedError;
 
@@ -6,5 +7,8 @@ use crate::errors::CheckedError;
 pub trait PayloadRetriever<T> {
     type Message;
     type Error: CheckedError;
-    async fn retrieve_event(&mut self, msg: &Self::Message) -> Result<Option<T>, Self::Error>;
+    async fn retrieve_event(
+        &mut self,
+        msg: &Self::Message,
+    ) -> Result<Option<(Metadata, T)>, Self::Error>;
 }
diff --git a/src/rust/sqs-executor/src/retriever/s3_event_retriever.rs b/src/rust/sqs-executor/src/retriever/s3_event_retriever.rs
index 36f8c639da..a0903bfe26 100644
--- a/src/rust/sqs-executor/src/retriever/s3_event_retriever.rs
+++ b/src/rust/sqs-executor/src/retriever/s3_event_retriever.rs
@@ -25,6 +25,7 @@ use rusoto_s3::{
     S3,
 };
 use rusoto_sqs::Message as SqsMessage;
+use rust_proto::pipeline::Metadata;
 use tokio::{
     io::AsyncReadExt,
     time::error::Elapsed,
@@ -118,6 +119,8 @@ where
     S3Error(#[from] RusotoError<GetObjectError>),
     #[error("Decode error")]
     DecodeError(#[from] DecoderErrorT),
+    #[error("EnvelopeDecode error")]
+    EnvelopeDecode(#[from] prost::DecodeError),
     #[error("IO")]
     Io(#[from] std::io::Error),
     #[error("JSON")]
@@ -134,6 +137,7 @@ where
         match self {
             Self::S3Error(_) => Recoverable::Transient,
             Self::DecodeError(_) => Recoverable::Persistent,
+            Self::EnvelopeDecode(_) => Recoverable::Persistent,
             Self::Io(_) => Recoverable::Transient,
             Self::Json(_) => Recoverable::Persistent,
             Self::Timeout(_) => Recoverable::Transient,
@@ -155,7 +159,10 @@ where
     type Error = S3PayloadRetrieverError<DecoderErrorT>;
 
     #[tracing::instrument(skip(self, msg))]
-    async fn retrieve_event(&mut self, msg: &Self::Message) -> Result<Option<E>, Self::Error> {
+    async fn retrieve_event(
+        &mut self,
+        msg: &Self::Message,
+    ) -> Result<Option<(Metadata, E)>, Self::Error> {
         let body = msg.body.as_ref().unwrap();
         debug!("Got body from message: {}", body);
         let event: serde_json::Value = serde_json::from_str(body)?;
@@ -241,6 +248,12 @@ where
 
         debug!("Read s3 payload body");
 
+        let envelope: rust_proto::pipeline::Envelope = prost::Message::decode(&body[..])?;
+        let meta = envelope
+            .metadata
+            .expect("Metadata must be set at the front of the pipeline");
+        let body = envelope.inner_message;
+
         let (decoded, ms) = time_it(|| self.decoder.decode(body));
 
         self.metric_reporter
@@ -254,6 +267,6 @@ where
                 |e| error!(message="failed to report s3_retriever.decoded.micros", error=?e),
             );
 
-        Ok(Some(decoded?))
+        Ok(Some((meta, decoded?)))
     }
 }


Test patch:
diff --git a/.buildkite/pipeline.verify.ami-test.yml b/.buildkite/pipeline.verify.ami-test.yml
deleted file mode 100644
index 6d3469ec04..0000000000
--- a/.buildkite/pipeline.verify.ami-test.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-# This file borrows heavily from the pattern defined in `pipeline-infrastructure`.
-
----
-env:
-  PANTS_CONFIG_FILES: "['pants.toml', 'pants.ci.toml']"
-
-# This is defined in a separate pipeline file because
-# chronotc/monorepo-diff does not currently have support for plugin
-# (or key) configuration on commands.
-#
-# :(
-
-steps:
-  - label: ":packer: AMI Test Build: Server"
-    command: ".buildkite/scripts/tests_in_ci/packer_integration_test.sh"
-    plugins:
-      - docker#v3.8.0:
-          image: "hashicorp/packer:1.7.2"
-          entrypoint: bash
-          propagate-aws-auth-tokens: true
-          environment:
-            - "BUILDKITE_BRANCH"
-            - "BUILDKITE_BUILD_NUMBER"
-            - "BUILDKITE_COMMIT"
-            - "BUILDKITE_PIPELINE_NAME"
-            - "BUILDKITE_STEP_KEY"
-            - "PACKER_IMAGE_NAME=grapl-nomad-consul-server"
-    agents:
-      queue: "packer-staging"
-
-  - label: ":packer: AMI Test Build: Client"
-    command: ".buildkite/scripts/tests_in_ci/packer_integration_test.sh"
-    plugins:
-      - docker#v3.8.0:
-          image: "hashicorp/packer:1.7.2"
-          entrypoint: bash
-          propagate-aws-auth-tokens: true
-          environment:
-            - "BUILDKITE_BRANCH"
-            - "BUILDKITE_BUILD_NUMBER"
-            - "BUILDKITE_COMMIT"
-            - "BUILDKITE_PIPELINE_NAME"
-            - "BUILDKITE_STEP_KEY"
-            - "PACKER_IMAGE_NAME=grapl-nomad-consul-client"
-    agents:
-      queue: "packer-staging"
diff --git a/.buildkite/scripts/lib/extract_ami_id_dict_test.sh b/.buildkite/scripts/lib/extract_ami_id_dict_test.sh
deleted file mode 100755
index 61f854d043..0000000000
--- a/.buildkite/scripts/lib/extract_ami_id_dict_test.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env bash
-
-readonly jq_filter_path=".buildkite/scripts/lib/extract_ami_id_dict.jq"
-readonly example_manifest_file=".buildkite/scripts/lib/example_packer_artifact.json"
-
-test_extract_ami_id_dict() {
-    packer_image_name="imgname"
-    actual=$(jq --raw-output --arg IMAGE_NAME ${packer_image_name} --from-file "${jq_filter_path}" "${example_manifest_file}")
-    expected=$(
-        cat << EOF
-{
-  "imgname.us-east-1": "ami-111",
-  "imgname.us-east-2": "ami-222",
-  "imgname.us-west-1": "ami-333",
-  "imgname.us-west-2": "ami-444"
-}
-EOF
-    )
-
-    assertEquals "Failed to generate expected JSON" \
-        "${expected}" \
-        "${actual}"
-}
diff --git a/.buildkite/scripts/tests_in_ci/packer_integration_test.sh b/.buildkite/scripts/tests_in_ci/packer_integration_test.sh
deleted file mode 100644
index ba33ce26d0..0000000000
--- a/.buildkite/scripts/tests_in_ci/packer_integration_test.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-# This test runs in `pipeline.verify.ami-test.yml.`
-# It is *NOT* shunit.
-
-# TODO: Deduplicate - https://github.com/grapl-security/issue-tracker/issues/614
-
-set -euo pipefail
-
-source .buildkite/scripts/lib/packer.sh
-
-echo -e "--- :packer: Performing test build of AMI"
-PACKER_VARS="-var build_ami=false" PACKER_LOG=1 build_ami "${PACKER_IMAGE_NAME}"
\ No newline at end of file
diff --git a/3rdparty/python/pytest.lockfile b/3rdparty/python/pytest.lockfile
new file mode 100644
index 0000000000..93b4a66faf
--- /dev/null
+++ b/3rdparty/python/pytest.lockfile
@@ -0,0 +1,112 @@
+# This lockfile was autogenerated by Pants. To regenerate, run:
+#
+#    ./pants generate-lockfiles --resolve=pytest
+#
+# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---
+# {
+#   "requirements_invalidation_digest": "ce37f87efbd9086c4701f14e54f61379c44fc0960e27d9db82ad001a83188275",
+#   "valid_for_interpreter_constraints": [
+#     "CPython==3.7.*"
+#   ]
+# }
+# --- END PANTS LOCKFILE METADATA ---
+
+atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") \
+    --hash=sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197 \
+    --hash=sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a
+attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
+    --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \
+    --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb
+colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") \
+    --hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 \
+    --hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b
+coverage==5.5; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" \
+    --hash=sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf \
+    --hash=sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b \
+    --hash=sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669 \
+    --hash=sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90 \
+    --hash=sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c \
+    --hash=sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a \
+    --hash=sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82 \
+    --hash=sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905 \
+    --hash=sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083 \
+    --hash=sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5 \
+    --hash=sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81 \
+    --hash=sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6 \
+    --hash=sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0 \
+    --hash=sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae \
+    --hash=sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb \
+    --hash=sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160 \
+    --hash=sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6 \
+    --hash=sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701 \
+    --hash=sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793 \
+    --hash=sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e \
+    --hash=sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3 \
+    --hash=sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066 \
+    --hash=sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a \
+    --hash=sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465 \
+    --hash=sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb \
+    --hash=sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821 \
+    --hash=sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45 \
+    --hash=sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184 \
+    --hash=sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a \
+    --hash=sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53 \
+    --hash=sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d \
+    --hash=sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638 \
+    --hash=sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3 \
+    --hash=sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a \
+    --hash=sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a \
+    --hash=sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6 \
+    --hash=sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2 \
+    --hash=sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759 \
+    --hash=sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873 \
+    --hash=sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a \
+    --hash=sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6 \
+    --hash=sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502 \
+    --hash=sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b \
+    --hash=sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529 \
+    --hash=sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b \
+    --hash=sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff \
+    --hash=sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b \
+    --hash=sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6 \
+    --hash=sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03 \
+    --hash=sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079 \
+    --hash=sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4 \
+    --hash=sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c
+importlib-metadata==4.8.1; python_version < "3.8" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") \
+    --hash=sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15 \
+    --hash=sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1
+iniconfig==1.1.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \
+    --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
+    --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32
+packaging==21.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \
+    --hash=sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14 \
+    --hash=sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7
+pluggy==1.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \
+    --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 \
+    --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159
+py==1.10.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \
+    --hash=sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a \
+    --hash=sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3
+pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" \
+    --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \
+    --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1
+pytest-cov==2.12.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
+    --hash=sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7 \
+    --hash=sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a
+pytest-custom-exit-code==0.3.0; (python_version > "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
+    --hash=sha256:51ffff0ee2c1ddcc1242e2ddb2a5fd02482717e33a2326ef330e3aa430244635 \
+    --hash=sha256:6e0ce6e57ce3a583cb7e5023f7d1021e19dfec22be41d9ad345bae2fc61caf3b
+pytest==6.2.5; python_version >= "3.6" \
+    --hash=sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134 \
+    --hash=sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89
+toml==0.10.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
+    --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+    --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+typing-extensions==3.10.0.2; python_version < "3.8" and python_version >= "3.6" \
+    --hash=sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7 \
+    --hash=sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34 \
+    --hash=sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e
+zipp==3.5.0; python_version < "3.8" and python_version >= "3.6" \
+    --hash=sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3 \
+    --hash=sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4
diff --git a/nomad/local/integration-tests.nomad b/nomad/local/integration-tests.nomad
index 5675046ccd..1d43b7b0b5 100644
--- a/nomad/local/integration-tests.nomad
+++ b/nomad/local/integration-tests.nomad
@@ -52,6 +52,16 @@ variable "test_user_name" {
   description = "The name of the test user"
 }
 
+variable "docker_user" {
+  type        = string
+  description = "The UID:GID pair to run as inside the Docker container"
+}
+
+variable "grapl_root" {
+  type        = string
+  description = "Where to find the Grapl repo on the host OS (where Nomad runs)."
+}
+
 locals {
   log_level = "DEBUG"
 
@@ -127,61 +137,18 @@ job "integration-tests" {
         REDIS_ENDPOINT = local.redis_endpoint
         KAFKA_ENDPOINT = local.kafka_endpoint
       }
-    }
-  }
-  group "analyzerlib-integration-tests" {
-    restart {
-      # Make this a one-shot job
-      attempts = 0
-    }
-
-    network {
-      mode = "bridge"
-    }
-
-    # Enable service discovery
-    service {
-      name = "analyzerlib-integration-tests"
-      connect {
-        sidecar_service {
-          proxy {
-            upstreams {
-              # This is a hack, because IDK how to share locals across files
-              destination_name = "dgraph-alpha-0-grpc-public"
-              local_bind_port  = 9080
-            }
-          }
-        }
-      }
-    }
-
-    task "analyzerlib-integration-tests" {
-      driver = "docker"
-
-      config {
-        image      = "${var.container_registry}grapl/analyzerlib-test:dev"
-        entrypoint = ["/bin/bash", "-o", "errexit", "-o", "nounset", "-c"]
-        command    = "cd grapl_analyzerlib && py.test -v -n auto -m 'integration_test'"
-      }
-
-      env {
-        DEPLOYMENT_NAME = var.deployment_name
-        GRAPL_LOG_LEVEL = local.log_level
-        MG_ALPHAS       = "localhost:9080"
-      }
 
+      # Because Cargo does some... compiling... for some reason.... maybe.....
       resources {
-        cpu    = 500
-        memory = 1024
+        memory = 8192
       }
-
     }
   }
 
-  group "analyzer-executor-integration-tests" {
+  group "python-integration-tests" {
     restart {
-      # Make this a one-shot job
-      attempts = 0
+      # I guess I can let it try 2x
+      attempts = 1
     }
 
     network {
@@ -190,7 +157,7 @@ job "integration-tests" {
 
     # Enable service discovery
     service {
-      name = "analyzer-executor-integration-tests"
+      name = "python-integration-tests"
       connect {
         sidecar_service {
           proxy {
@@ -199,111 +166,70 @@ job "integration-tests" {
               destination_name = "dgraph-alpha-0-grpc-public"
               local_bind_port  = 9080
             }
+            upstreams {
+              destination_name = "web-ui"
+              local_bind_port  = 1234
+            }
           }
         }
       }
     }
-    task "analyzer-executor-integration-tests" {
+
+    task "python-integration-tests" {
       driver = "docker"
+      user   = var.docker_user
 
       config {
-        image      = "${var.container_registry}grapl/analyzer-executor-test:dev"
-        entrypoint = ["/bin/bash", "-o", "errexit", "-o", "nounset", "-c"]
-        command    = "cd analyzer_executor && export PYTHONPATH=\"$(pwd)/src\"; py.test -n auto -m 'integration_test'"
+        image = "${var.container_registry}grapl/python-integration-tests:dev"
+        # Pants caches requirements per-user. So when we run a Docker container
+        # with the host's userns, this lets us reuse the pants cache.
+        # (This descreases runtime on my personal laptop from 390s to 190s)
+        userns_mode = "host"
+
+        mount {
+          # Just to clarify, this is all Docker-verbiage mounts and binds.
+          # Nothing Nomad-y about it.
+          type     = "bind"
+          source   = var.grapl_root
+          target   = "/mnt/grapl-root"
+          readonly = false
+        }
       }
 
       env {
-        # aws vars
-        AWS_REGION                  = var.aws_region
-        GRAPL_AWS_ENDPOINT          = local.aws_endpoint
-        GRAPL_AWS_ACCESS_KEY_ID     = var.aws_access_key_id
-        GRAPL_AWS_ACCESS_KEY_SECRET = var.aws_access_key_secret
-
-        GRAPL_LOG_LEVEL = local.log_level
+        AWS_REGION                  = "${var.aws_region}"
+        GRAPL_AWS_ENDPOINT          = "${local.aws_endpoint}"
+        GRAPL_AWS_ACCESS_KEY_ID     = "${var.aws_access_key_id}"
+        GRAPL_AWS_ACCESS_KEY_SECRET = "${var.aws_access_key_secret}"
 
         # These environment vars need to exist but the values aren't actually exercised
         GRAPL_ANALYZER_MATCHED_SUBGRAPHS_BUCKET = "NOT_ACTUALLY_EXERCISED_IN_TESTS"
         GRAPL_ANALYZERS_BUCKET                  = "NOT_ACTUALLY_EXERCISED_IN_TESTS"
         GRAPL_MODEL_PLUGINS_BUCKET              = "NOT_ACTUALLY_EXERCISED_IN_TESTS"
 
-        HITCACHE_ADDR     = local.redis_host
-        HITCACHE_PORT     = local.redis_port
-        MESSAGECACHE_ADDR = local.redis_host
-        MESSAGECACHE_PORT = local.redis_port
-        IS_RETRY          = false
-      }
-
-      resources {
-        cpu    = 500
-        memory = 1024
-      }
-
-    }
-  }
-
-  group "graphql-endpoint-tests" {
-    restart {
-      # Make this a one-shot job
-      attempts = 0
-    }
+        GRAPL_API_HOST                = "localhost"
+        GRAPL_HTTP_FRONTEND_PORT      = "${NOMAD_UPSTREAM_PORT_web-ui}"
+        GRAPL_TEST_USER_NAME          = "${var.test_user_name}"
+        GRAPL_SCHEMA_PROPERTIES_TABLE = "${var.schema_properties_table_name}"
 
-    network {
-      mode = "bridge"
-    }
+        HITCACHE_ADDR     = "${local.redis_host}"
+        HITCACHE_PORT     = "${local.redis_port}"
+        MESSAGECACHE_ADDR = "${local.redis_host}"
+        MESSAGECACHE_PORT = "${local.redis_port}"
+        IS_RETRY          = "False"
+        IS_LOCAL          = "True"
 
-    # Enable service discovery
-    service {
-      name = "graphql-endpoint-tests"
-      connect {
-        sidecar_service {
-          proxy {
-            upstreams {
-              # This is a hack, because IDK how to share locals across files
-              destination_name = "dgraph-alpha-0-grpc-public"
-              local_bind_port  = 9080
-            }
-            upstreams {
-              destination_name = "web-ui"
-              local_bind_port  = 1234
-            }
-          }
-        }
-      }
-    }
-    task "graphql-endpoint-tests" {
-      driver = "docker"
+        DEPLOYMENT_NAME = "${var.deployment_name}"
+        GRAPL_LOG_LEVEL = "${local.log_level}"
+        MG_ALPHAS       = "localhost:9080"
 
-      config {
-        image      = "${var.container_registry}grapl/graphql-endpoint-tests:dev"
-        entrypoint = ["/bin/bash", "-o", "errexit", "-o", "nounset", "-c"]
-        command    = "cd graphql_endpoint_tests; py.test --capture=no -n 1 -m 'integration_test'"
       }
 
-      env {
-        # aws vars
-        AWS_REGION                  = var.aws_region
-        GRAPL_AWS_ENDPOINT          = local.aws_endpoint
-        GRAPL_AWS_ACCESS_KEY_ID     = var.aws_access_key_id
-        GRAPL_AWS_ACCESS_KEY_SECRET = var.aws_access_key_secret
-
-        DEPLOYMENT_NAME = var.deployment_name
-        GRAPL_LOG_LEVEL = local.log_level
-
-        GRAPL_SCHEMA_PROPERTIES_TABLE = var.schema_properties_table_name
-
-        # These are placeholders since Ian is replacing the nginx service shortly
-        GRAPL_API_HOST           = "localhost"
-        GRAPL_HTTP_FRONTEND_PORT = "${NOMAD_UPSTREAM_PORT_web-ui}"
-        GRAPL_TEST_USER_NAME     = var.test_user_name
-
-        IS_LOCAL  = true
-        MG_ALPHAS = "localhost:9080"
+      resources {
+        memory = 2048
       }
     }
-
-
   }
 
-  # TODO: `web-ui-integration-tests`
 }
 
diff --git a/pulumi/tests/BUILD b/pulumi/tests/BUILD
index 5ad13ea457..5663e86609 100644
--- a/pulumi/tests/BUILD
+++ b/pulumi/tests/BUILD
@@ -1,6 +1,8 @@
 python_tests(
     name="tests",
-    extra_env_vars=["GRAPL_OPERATIONAL_ALARMS_EMAIL=fake-ops-email@fake.domain"],
+    extra_env_vars=[
+        "GRAPL_OPERATIONAL_ALARMS_EMAIL=fake-ops-email@fake.domain",
+    ],
     dependencies=[
         '3rdparty/python:setuptools'
     ],
diff --git a/src/python/analyzer_executor/tests/BUILD b/src/python/analyzer_executor/tests/BUILD
index dabf212d7e..3ac0f07da0 100644
--- a/src/python/analyzer_executor/tests/BUILD
+++ b/src/python/analyzer_executor/tests/BUILD
@@ -1,1 +1,13 @@
-python_tests()
+python_tests(
+    extra_env_vars=[
+        "HITCACHE_ADDR",
+        "HITCACHE_PORT",
+        "MESSAGECACHE_ADDR",
+        "MESSAGECACHE_PORT",
+        "GRAPL_ANALYZERS_BUCKET",
+        "GRAPL_ANALYZER_MATCHED_SUBGRAPHS_BUCKET",
+        "GRAPL_LOG_LEVEL",
+        "GRAPL_MODEL_PLUGINS_BUCKET",
+        "IS_RETRY",
+    ]
+)
diff --git a/src/python/graphql_endpoint_tests/BUILD b/src/python/graphql_endpoint_tests/BUILD
index ce26cb2936..cb8b3828e4 100644
--- a/src/python/graphql_endpoint_tests/BUILD
+++ b/src/python/graphql_endpoint_tests/BUILD
@@ -1,6 +1,22 @@
 python_library()
 
 python_tests(
-  name="tests",
-  sources=["**/*.py"]
+    name="graphql-endpoint-tests",
+    sources=["**/*.py"],
+    extra_env_vars=[
+        "AWS_REGION",
+        "DEBUG_SERVICES",
+        "DEPLOYMENT_NAME",
+        "GRAPL_API_HOST",
+        "GRAPL_AWS_ACCESS_KEY_ID",
+        "GRAPL_AWS_ACCESS_KEY_SECRET",
+        "GRAPL_AWS_ENDPOINT",
+        "GRAPL_HTTP_FRONTEND_PORT",
+        "GRAPL_LOG_LEVEL",
+        "GRAPL_SCHEMA_PROPERTIES_TABLE",
+        "GRAPL_TEST_USER_NAME",
+        "IS_LOCAL",
+        "MG_ALPHAS",
+        "VSC_DEBUGGER_PORT",
+    ]
 )
diff --git a/src/python/grapl-tests-common/BUILD b/src/python/grapl-tests-common/BUILD
index deb958ec47..893ef48365 100644
--- a/src/python/grapl-tests-common/BUILD
+++ b/src/python/grapl-tests-common/BUILD
@@ -2,11 +2,11 @@ grapl_python_distribution(
     dependencies = [
         "./grapl_tests_common",
         "./grapl_tests_common:py_typed",
-        "./grapl_tests_common/clients"
+        "./grapl_tests_common/clients",
     ],
     provides = setup_py(
         name = "grapl-tests-common",
-        version = "0.0.5",
+        version = "0.0.6",
         description = "Shared code between Grapl end-to-end tests"
     )
 )
diff --git a/src/python/grapl-tests-common/grapl_tests_common/upload_logs.py b/src/python/grapl-tests-common/grapl_tests_common/upload_logs.py
index b213c95d42..11f4d860aa 100755
--- a/src/python/grapl-tests-common/grapl_tests_common/upload_logs.py
+++ b/src/python/grapl-tests-common/grapl_tests_common/upload_logs.py
@@ -4,6 +4,7 @@
 import random
 import string
 import time
+import uuid
 from dataclasses import dataclass
 from datetime import datetime
 from os import PathLike
@@ -11,6 +12,7 @@
 from typing import TYPE_CHECKING, Iterator, List, Optional, cast
 
 from grapl_common.env_helpers import S3ClientFactory, SQSClientFactory
+from python_proto.pipeline import Envelope, Metadata
 
 if TYPE_CHECKING:
     from mypy_boto3_s3 import S3Client
@@ -144,7 +146,16 @@ def chunker(seq: List[bytes], size: int) -> Iterator[List[bytes]]:
             + str(epoch)
             + rand_str(6)
         )
-        s3.put_object(Body=chunk_body, Bucket=bucket, Key=key)
+        envelope = Envelope(
+            metadata=Metadata(
+                tenant_id=uuid.uuid4(),  # FIXME: be smarter here.
+                trace_id=uuid.uuid4(),  # FIXME: and here.
+            ),
+            inner_message=chunk_body,
+            inner_type="RawEvents",
+        )
+
+        s3.put_object(Body=envelope.serialize(), Bucket=bucket, Key=key)
 
         # local-grapl relies on manual eventing
         if requires_manual_eventing:
diff --git a/src/python/grapl-tests-common/setup.py b/src/python/grapl-tests-common/setup.py
deleted file mode 100644
index bca3fc6f32..0000000000
--- a/src/python/grapl-tests-common/setup.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# type: ignore
-import os
-
-from setuptools import find_packages, setup
-
-with open("README.md") as readme:
-    README = readme.read()
-
-HERE = os.path.abspath(os.path.dirname(__file__))
-
-
-def find_version():
-    with open(os.path.join(HERE, "VERSION")) as version:
-        return version.read().strip()
-
-
-__version__ = find_version()
-
-
-setup(
-    name="grapl-tests-common",
-    version=__version__,
-    description="Shared code between Grapl end-to-end tests",
-    long_description=README,
-    url="https://github.com/grapl-security/grapl",
-    author="Max Wittek",
-    author_email="wimax@graplsecurity.com",
-    license="MIT",
-    classifiers=[
-        "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.7",
-    ],
-    zip_safe=False,
-    packages=find_packages(),
-    package_data={
-        "grapl_tests_common": [
-            "py.typed",
-        ]
-    },
-    include_package_data=True,
-    install_requires=[
-        "requests",
-        "typing_extensions",
-        "zstd==1.4.9.1",
-    ],
-    # We'll probably have some dataclasses in here in the future
-    python_requires=">=3.6",
-    extras_require={
-        "typecheck": ["mypy", "boto3-stubs[dynamodb]"],
-    },
-)
diff --git a/src/python/grapl_analyzerlib/tests/BUILD b/src/python/grapl_analyzerlib/tests/BUILD
index bdfb471676..68699a07ef 100644
--- a/src/python/grapl_analyzerlib/tests/BUILD
+++ b/src/python/grapl_analyzerlib/tests/BUILD
@@ -1,3 +1,8 @@
 python_tests(
-    skip_mypy=True
+    skip_mypy=True,
+    extra_env_vars=[
+        "GRAPL_LOG_LEVEL",
+        "DEPLOYMENT_NAME",
+        "MG_ALPHAS",
+    ]
 )
diff --git a/src/python/graplctl/tests/BUILD b/src/python/graplctl/tests/BUILD
index 1b4488e416..8b2faacde0 100644
--- a/src/python/graplctl/tests/BUILD
+++ b/src/python/graplctl/tests/BUILD
@@ -1,6 +1,6 @@
 python_tests(
     dependencies=[
-        "//etc/sample_data",
+        "etc/sample_data",
     ]
 )
 
diff --git a/src/python/graplinc-grapl-api/pytest.ini b/src/python/graplinc-grapl-api/pytest.ini
deleted file mode 100644
index 223ea5fb73..0000000000
--- a/src/python/graplinc-grapl-api/pytest.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[pytest]
-markers =
-    integration_test: marks test as integration_test (deselect with '-m "not integration_test"')
diff --git a/src/python/graplinc-grapl-api/test_graph_descriptions.py b/src/python/graplinc-grapl-api/test_graph_descriptions.py
deleted file mode 100644
index 2a56abfcab..0000000000
--- a/src/python/graplinc-grapl-api/test_graph_descriptions.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from graplinc.grapl.api.graph.v1beta1 import *
-
-
-def test_noop():
-    return
diff --git a/src/python/python-proto/python_proto/tests/BUILD b/src/python/python-proto/python_proto/tests/BUILD
new file mode 100644
index 0000000000..617c664298
--- /dev/null
+++ b/src/python/python-proto/python_proto/tests/BUILD
@@ -0,0 +1,5 @@
+python_library()
+
+python_tests(
+    name="python_proto_tests"
+)
diff --git a/src/python/python-proto/python_proto/tests/__init__.py b/src/python/python-proto/python_proto/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/python/python-proto/python_proto/tests/helpers.py b/src/python/python-proto/python_proto/tests/helpers.py
new file mode 100644
index 0000000000..48b2be00c3
--- /dev/null
+++ b/src/python/python-proto/python_proto/tests/helpers.py
@@ -0,0 +1,12 @@
+import hypothesis.strategies as st
+from hypothesis import given
+from python_proto import SerDe
+
+
+def check_encode_decode_invariant(strategy: st.SearchStrategy[SerDe]) -> None:
+    @given(strategy)
+    def _check_encode_decode_invariant(serde_value: SerDe) -> None:
+        transformed = serde_value.__class__.deserialize(serde_value.serialize())
+        assert transformed == serde_value
+
+    _check_encode_decode_invariant()
diff --git a/src/python/python-proto/python_proto/tests/strategies.py b/src/python/python-proto/python_proto/tests/strategies.py
new file mode 100644
index 0000000000..e8d17bf70f
--- /dev/null
+++ b/src/python/python-proto/python_proto/tests/strategies.py
@@ -0,0 +1,401 @@
+import uuid
+from typing import Mapping, Sequence, Union
+
+import hypothesis.strategies as st
+from python_proto.api import (
+    DecrementOnlyIntProp,
+    DecrementOnlyUintProp,
+    Edge,
+    EdgeList,
+    GraphDescription,
+    IdentifiedGraph,
+    IdentifiedNode,
+    IdStrategy,
+    ImmutableIntProp,
+    ImmutableStrProp,
+    ImmutableUintProp,
+    IncrementOnlyIntProp,
+    IncrementOnlyUintProp,
+    MergedEdge,
+    MergedEdgeList,
+    MergedGraph,
+    MergedNode,
+    NodeDescription,
+    NodeProperty,
+    Session,
+    Static,
+)
+from python_proto.metrics import (
+    Counter,
+    Gauge,
+    GaugeType,
+    Histogram,
+    Label,
+    MetricWrapper,
+)
+from python_proto.pipeline import Envelope, Metadata, Uuid
+
+UINT64_MIN = 0
+UINT64_MAX = 2 ** 64 - 1
+INT64_MIN = -(2 ** 63) - 1
+INT64_MAX = 2 ** 63 - 1
+
+#
+# pipeline
+#
+
+
+def uuids(
+    lsbs: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+    msbs: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+) -> st.SearchStrategy[Uuid]:
+    return st.builds(Uuid, lsb=lsbs, msb=msbs)
+
+
+def metadatas(
+    trace_ids: st.SearchStrategy[uuid.UUID] = st.uuids(),
+    tenant_ids: st.SearchStrategy[uuid.UUID] = st.uuids(),
+) -> st.SearchStrategy[Metadata]:
+    return st.builds(Metadata, trace_id=trace_ids, tenant_id=tenant_ids)
+
+
+def envelopes(
+    metadatas: st.SearchStrategy[Metadata] = metadatas(),
+    inner_messages: st.SearchStrategy[bytes] = st.binary(),
+    inner_types: st.SearchStrategy[str] = st.text(),
+) -> st.SearchStrategy[Envelope]:
+    return st.builds(
+        Envelope,
+        metadata=metadatas,
+        inner_message=inner_messages,
+        inner_type=inner_types,
+    )
+
+
+#
+# api
+#
+
+
+def sessions(
+    primary_key_properties: st.SearchStrategy[Sequence[str]] = st.lists(st.text()),
+    primary_key_requires_asset_ids: st.SearchStrategy[bool] = st.booleans(),
+    create_times: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+    last_seen_times: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+    terminate_times: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+) -> st.SearchStrategy[Session]:
+    return st.builds(
+        Session,
+        primary_key_properties=primary_key_properties,
+        primary_key_requires_asset_id=primary_key_requires_asset_ids,
+        create_time=create_times,
+        last_seen_time=last_seen_times,
+        terminate_time=terminate_times,
+    )
+
+
+def statics(
+    primary_key_properties: st.SearchStrategy[Sequence[str]] = st.lists(st.text()),
+    primary_key_requires_asset_ids: st.SearchStrategy[bool] = st.booleans(),
+) -> st.SearchStrategy[Static]:
+    return st.builds(
+        Static,
+        primary_key_properties=primary_key_properties,
+        primary_key_requires_asset_id=primary_key_requires_asset_ids,
+    )
+
+
+def id_strategies(
+    strategies: st.SearchStrategy[Union[Session, Static]] = st.one_of(
+        sessions(), statics()
+    )
+) -> st.SearchStrategy[IdStrategy]:
+    return st.builds(
+        IdStrategy,
+        strategy=strategies,
+    )
+
+
+def increment_only_uint_props(
+    props: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+) -> st.SearchStrategy[IncrementOnlyUintProp]:
+    return st.builds(IncrementOnlyUintProp, prop=props)
+
+
+def immutable_uint_props(
+    props: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+) -> st.SearchStrategy[ImmutableUintProp]:
+    return st.builds(ImmutableUintProp, prop=props)
+
+
+def decrement_only_uint_props(
+    props: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+) -> st.SearchStrategy[DecrementOnlyUintProp]:
+    return st.builds(DecrementOnlyUintProp, prop=props)
+
+
+def increment_only_int_props(
+    props: st.SearchStrategy[int] = st.integers(
+        min_value=INT64_MIN, max_value=INT64_MAX
+    ),
+) -> st.SearchStrategy[IncrementOnlyIntProp]:
+    return st.builds(IncrementOnlyIntProp, prop=props)
+
+
+def immutable_int_props(
+    props: st.SearchStrategy[int] = st.integers(
+        min_value=INT64_MIN, max_value=INT64_MAX
+    ),
+) -> st.SearchStrategy[ImmutableIntProp]:
+    return st.builds(ImmutableIntProp, prop=props)
+
+
+def decrement_only_int_props(
+    props: st.SearchStrategy[int] = st.integers(
+        min_value=INT64_MIN, max_value=INT64_MAX
+    ),
+) -> st.SearchStrategy[DecrementOnlyIntProp]:
+    return st.builds(DecrementOnlyIntProp, prop=props)
+
+
+def immutable_str_props(
+    props: st.SearchStrategy[str] = st.text(),
+) -> st.SearchStrategy[ImmutableStrProp]:
+    return st.builds(ImmutableStrProp, prop=props)
+
+
+def node_properties(
+    properties: st.SearchStrategy[
+        Union[
+            IncrementOnlyUintProp,
+            DecrementOnlyUintProp,
+            ImmutableUintProp,
+            IncrementOnlyIntProp,
+            DecrementOnlyIntProp,
+            ImmutableIntProp,
+            ImmutableStrProp,
+        ]
+    ] = st.one_of(
+        increment_only_uint_props(),
+        decrement_only_uint_props(),
+        immutable_uint_props(),
+        increment_only_int_props(),
+        decrement_only_int_props(),
+        immutable_int_props(),
+        immutable_str_props(),
+    )
+) -> st.SearchStrategy[NodeProperty]:
+    return st.builds(NodeProperty, property_=properties)
+
+
+def node_descriptions(
+    properties: st.SearchStrategy[Mapping[str, NodeProperty]] = st.dictionaries(
+        keys=st.text(), values=node_properties()
+    ),
+    node_keys: st.SearchStrategy[str] = st.text(),
+    node_types: st.SearchStrategy[str] = st.text(),
+    id_strategies: st.SearchStrategy[Sequence[IdStrategy]] = st.lists(id_strategies()),
+) -> st.SearchStrategy[NodeDescription]:
+    return st.builds(
+        NodeDescription,
+        properties=properties,
+        node_key=node_keys,
+        node_type=node_types,
+        id_strategy=id_strategies,
+    )
+
+
+def identified_nodes(
+    properties: st.SearchStrategy[Mapping[str, NodeProperty]] = st.dictionaries(
+        keys=st.text(), values=node_properties()
+    ),
+    node_keys: st.SearchStrategy[str] = st.text(),
+    node_types: st.SearchStrategy[str] = st.text(),
+) -> st.SearchStrategy[IdentifiedNode]:
+    return st.builds(
+        IdentifiedNode,
+        properties=properties,
+        node_key=node_keys,
+        node_type=node_types,
+    )
+
+
+def merged_nodes(
+    properties: st.SearchStrategy[Mapping[str, NodeProperty]] = st.dictionaries(
+        keys=st.text(), values=node_properties()
+    ),
+    uids: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+    node_keys: st.SearchStrategy[str] = st.text(),
+    node_types: st.SearchStrategy[str] = st.text(),
+) -> st.SearchStrategy[MergedNode]:
+    return st.builds(
+        MergedNode,
+        properties=properties,
+        uid=uids,
+        node_key=node_keys,
+        node_type=node_types,
+    )
+
+
+def edges(
+    from_node_keys: st.SearchStrategy[str] = st.text(),
+    to_node_keys: st.SearchStrategy[str] = st.text(),
+    edge_names: st.SearchStrategy[str] = st.text(),
+) -> st.SearchStrategy[Edge]:
+    return st.builds(
+        Edge,
+        from_node_key=from_node_keys,
+        to_node_key=to_node_keys,
+        edge_name=edge_names,
+    )
+
+
+def edge_lists(
+    edges: st.SearchStrategy[Sequence[Edge]] = st.lists(edges()),
+) -> st.SearchStrategy[EdgeList]:
+    return st.builds(
+        EdgeList,
+        edges=edges,
+    )
+
+
+def merged_edges(
+    from_uids: st.SearchStrategy[str] = st.text(),
+    from_node_keys: st.SearchStrategy[str] = st.text(),
+    to_uids: st.SearchStrategy[str] = st.text(),
+    to_node_keys: st.SearchStrategy[str] = st.text(),
+    edge_names: st.SearchStrategy[str] = st.text(),
+) -> st.SearchStrategy[MergedEdge]:
+    return st.builds(
+        MergedEdge,
+        from_uid=from_uids,
+        from_node_key=from_node_keys,
+        to_uid=to_uids,
+        to_node_key=to_node_keys,
+        edge_name=edge_names,
+    )
+
+
+def merged_edge_lists(
+    edges: st.SearchStrategy[Sequence[MergedEdge]] = st.lists(merged_edges()),
+) -> st.SearchStrategy[MergedEdgeList]:
+    return st.builds(
+        MergedEdgeList,
+        edges=edges,
+    )
+
+
+def graph_descriptions(
+    nodes: st.SearchStrategy[Mapping[str, NodeDescription]] = st.dictionaries(
+        keys=st.text(), values=node_descriptions()
+    ),
+    edges: st.SearchStrategy[Mapping[str, EdgeList]] = st.dictionaries(
+        keys=st.text(), values=edge_lists()
+    ),
+) -> st.SearchStrategy[GraphDescription]:
+    return st.builds(
+        GraphDescription,
+        nodes=nodes,
+        edges=edges,
+    )
+
+
+def identified_graphs(
+    nodes: st.SearchStrategy[Mapping[str, IdentifiedNode]] = st.dictionaries(
+        keys=st.text(), values=identified_nodes()
+    ),
+    edges: st.SearchStrategy[Mapping[str, EdgeList]] = st.dictionaries(
+        keys=st.text(), values=edge_lists()
+    ),
+) -> st.SearchStrategy[IdentifiedGraph]:
+    return st.builds(
+        IdentifiedGraph,
+        nodes=nodes,
+        edges=edges,
+    )
+
+
+def merged_graphs(
+    nodes: st.SearchStrategy[Mapping[str, MergedNode]] = st.dictionaries(
+        keys=st.text(), values=merged_nodes()
+    ),
+    edges: st.SearchStrategy[Mapping[str, MergedEdgeList]] = st.dictionaries(
+        keys=st.text(), values=merged_edge_lists()
+    ),
+) -> st.SearchStrategy[MergedGraph]:
+    return st.builds(
+        MergedGraph,
+        nodes=nodes,
+        edges=edges,
+    )
+
+
+#
+# metrics
+#
+
+
+def labels(
+    keys: st.SearchStrategy[str] = st.text(),
+    values: st.SearchStrategy[str] = st.text(),
+) -> st.SearchStrategy[Label]:
+    return st.builds(Label, key=keys, value=values)
+
+
+def counters(
+    names: st.SearchStrategy[str] = st.text(),
+    increments: st.SearchStrategy[int] = st.integers(
+        min_value=UINT64_MIN, max_value=UINT64_MAX
+    ),
+    labels: st.SearchStrategy[Sequence[Label]] = st.lists(labels()),
+) -> st.SearchStrategy[Counter]:
+    return st.builds(Counter, name=names, increment=increments, labels=labels)
+
+
+def gauge_types() -> st.SearchStrategy[GaugeType]:
+    return st.sampled_from(GaugeType)
+
+
+def gauges(
+    gauge_types: st.SearchStrategy[GaugeType] = gauge_types(),
+    names: st.SearchStrategy[str] = st.text(),
+    values: st.SearchStrategy[float] = st.floats(allow_nan=False, allow_infinity=False),
+    labels: st.SearchStrategy[Sequence[Label]] = st.lists(labels()),
+) -> st.SearchStrategy[Gauge]:
+    return st.builds(
+        Gauge, gauge_type=gauge_types, name=names, value=values, labels=labels
+    )
+
+
+def histograms(
+    names: st.SearchStrategy[str] = st.text(),
+    values: st.SearchStrategy[float] = st.floats(allow_nan=False, allow_infinity=False),
+    labels: st.SearchStrategy[Sequence[Label]] = st.lists(labels()),
+) -> st.SearchStrategy[Histogram]:
+    return st.builds(Histogram, name=names, value=values, labels=labels)
+
+
+def metric_wrappers(
+    metrics: st.SearchStrategy[Union[Counter, Gauge, Histogram]] = st.one_of(
+        counters(), gauges(), histograms()
+    )
+) -> st.SearchStrategy[MetricWrapper]:
+    return st.builds(MetricWrapper, metric=metrics)
diff --git a/src/python/python-proto/python_proto/tests/test_api.py b/src/python/python-proto/python_proto/tests/test_api.py
new file mode 100644
index 0000000000..ae17911406
--- /dev/null
+++ b/src/python/python-proto/python_proto/tests/test_api.py
@@ -0,0 +1,112 @@
+import pytest
+
+pytest.register_assert_rewrite("python_proto.tests.helpers")
+
+from python_proto.tests.helpers import check_encode_decode_invariant
+from python_proto.tests.strategies import (
+    decrement_only_int_props,
+    decrement_only_uint_props,
+    edge_lists,
+    edges,
+    graph_descriptions,
+    id_strategies,
+    identified_graphs,
+    identified_nodes,
+    immutable_int_props,
+    immutable_str_props,
+    immutable_uint_props,
+    increment_only_int_props,
+    increment_only_uint_props,
+    merged_edge_lists,
+    merged_edges,
+    merged_graphs,
+    merged_nodes,
+    node_descriptions,
+    node_properties,
+    sessions,
+    statics,
+)
+
+
+def test_decrement_only_int_prop_encode_decode() -> None:
+    check_encode_decode_invariant(decrement_only_int_props())
+
+
+def test_decrement_only_uint_prop_encode_decode() -> None:
+    check_encode_decode_invariant(decrement_only_uint_props())
+
+
+def test_edge_list_encode_decode() -> None:
+    check_encode_decode_invariant(edge_lists())
+
+
+def test_edge_encode_decode() -> None:
+    check_encode_decode_invariant(edges())
+
+
+def test_graph_description_encode_decode() -> None:
+    check_encode_decode_invariant(graph_descriptions())
+
+
+def test_id_strategy_encode_decode() -> None:
+    check_encode_decode_invariant(id_strategies())
+
+
+def test_identified_graph_encode_decode() -> None:
+    check_encode_decode_invariant(identified_graphs())
+
+
+def test_identified_node_encode_decode() -> None:
+    check_encode_decode_invariant(identified_nodes())
+
+
+def test_immutable_int_prop_encode_decode() -> None:
+    check_encode_decode_invariant(immutable_int_props())
+
+
+def test_immutable_str_prop_encode_decode() -> None:
+    check_encode_decode_invariant(immutable_str_props())
+
+
+def test_immutable_uint_prop_encode_decode() -> None:
+    check_encode_decode_invariant(immutable_uint_props())
+
+
+def test_increment_only_int_prop_encode_decode() -> None:
+    check_encode_decode_invariant(increment_only_int_props())
+
+
+def test_increment_only_uint_prop_encode_decode() -> None:
+    check_encode_decode_invariant(increment_only_uint_props())
+
+
+def test_merged_edge_list_encode_decode() -> None:
+    check_encode_decode_invariant(merged_edge_lists())
+
+
+def test_merged_edge_encode_decode() -> None:
+    check_encode_decode_invariant(merged_edges())
+
+
+def test_merged_graph_encode_decode() -> None:
+    check_encode_decode_invariant(merged_graphs())
+
+
+def test_merged_node_encode_decode() -> None:
+    check_encode_decode_invariant(merged_nodes())
+
+
+def test_node_description_encode_decode() -> None:
+    check_encode_decode_invariant(node_descriptions())
+
+
+def test_node_property_encode_decode() -> None:
+    check_encode_decode_invariant(node_properties())
+
+
+def test_session_encode_decode() -> None:
+    check_encode_decode_invariant(sessions())
+
+
+def test_static_encode_decode() -> None:
+    check_encode_decode_invariant(statics())
diff --git a/src/python/python-proto/python_proto/tests/test_metrics.py b/src/python/python-proto/python_proto/tests/test_metrics.py
new file mode 100644
index 0000000000..2067b66928
--- /dev/null
+++ b/src/python/python-proto/python_proto/tests/test_metrics.py
@@ -0,0 +1,32 @@
+import pytest
+
+pytest.register_assert_rewrite("python_proto.tests.helpers")
+
+from python_proto.tests.helpers import check_encode_decode_invariant
+from python_proto.tests.strategies import (
+    counters,
+    gauges,
+    histograms,
+    labels,
+    metric_wrappers,
+)
+
+
+def test_label_encode_decode() -> None:
+    check_encode_decode_invariant(labels())
+
+
+def test_counter_encode_decode() -> None:
+    check_encode_decode_invariant(counters())
+
+
+def test_gauge_encode_decode() -> None:
+    check_encode_decode_invariant(gauges())
+
+
+def test_histogram_encode_decode() -> None:
+    check_encode_decode_invariant(histograms())
+
+
+def test_metric_wrapper_encode_decode() -> None:
+    check_encode_decode_invariant(metric_wrappers())
diff --git a/src/python/python-proto/python_proto/tests/test_pipeline.py b/src/python/python-proto/python_proto/tests/test_pipeline.py
new file mode 100644
index 0000000000..ad12e51d9e
--- /dev/null
+++ b/src/python/python-proto/python_proto/tests/test_pipeline.py
@@ -0,0 +1,28 @@
+import uuid
+
+import pytest
+
+pytest.register_assert_rewrite("python_proto.tests.helpers")
+
+from hypothesis import given
+from hypothesis import strategies as st
+from python_proto.pipeline import Uuid
+from python_proto.tests.helpers import check_encode_decode_invariant
+from python_proto.tests.strategies import envelopes, metadatas, uuids
+
+
+def test_uuid_encode_decode() -> None:
+    check_encode_decode_invariant(uuids())
+
+
+@given(st.uuids())
+def test_uuid_from_into(uuid_: uuid.UUID) -> None:
+    assert Uuid.from_uuid(uuid_).into_uuid() == uuid_
+
+
+def test_metadata_encode_decode() -> None:
+    check_encode_decode_invariant(metadatas())
+
+
+def test_envelope_encode_decode() -> None:
+    check_encode_decode_invariant(envelopes())
diff --git a/src/python/python_test_deps/README.md b/src/python/python_test_deps/README.md
deleted file mode 100644
index 28aefd9ac2..0000000000
--- a/src/python/python_test_deps/README.md
+++ /dev/null
@@ -1,6 +0,0 @@
-Based off https://stackoverflow.com/a/14447068
-
-Basically:
-
-- Build an image that has test dependencies downloaded
-- Expose a script for "client images" to pull them into their venv
diff --git a/src/python/python_test_deps/download_requirements.sh b/src/python/python_test_deps/download_requirements.sh
deleted file mode 100755
index 55c328381e..0000000000
--- a/src/python/python_test_deps/download_requirements.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-set -eu
-THIS_DIR="/home/grapl/python_test_deps"
-cd /home/grapl
-source venv/bin/activate 
-
-cd $THIS_DIR
-pip download -r requirements.txt
diff --git a/src/python/python_test_deps/install_requirements.sh b/src/python/python_test_deps/install_requirements.sh
deleted file mode 100755
index 0ec928931e..0000000000
--- a/src/python/python_test_deps/install_requirements.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-set -eu
-THIS_DIR="/home/grapl/python_test_deps"
-
-source /home/grapl/venv/bin/activate
-pip install --no-index --find-links $THIS_DIR -r $THIS_DIR/requirements.txt
diff --git a/src/python/python_test_deps/requirements.txt b/src/python/python_test_deps/requirements.txt
deleted file mode 100644
index 872b895b7f..0000000000
--- a/src/python/python_test_deps/requirements.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-pytest==6.2.1
-pytest-xdist==2.2.0
-hypothesis==6.0.2
-
-mypy==0.790
-pytype==2021.1.14
-boto3-stubs[essential,sagemaker,secretsmanager]==1.16.54.0
diff --git a/src/rust/derive-dynamic-node/tests/derive_session.rs b/src/rust/derive-dynamic-node/tests/derive_session.rs
index e42026b7d5..37ae57812f 100644
--- a/src/rust/derive-dynamic-node/tests/derive_session.rs
+++ b/src/rust/derive-dynamic-node/tests/derive_session.rs
@@ -2,7 +2,7 @@ use derive_dynamic_node::{
     GraplSessionId,
     NodeDescription,
 };
-use grapl_graph_descriptions::graph_description::*;
+use rust_proto::graph_descriptions::*;
 
 #[derive(NodeDescription, GraplSessionId)]
 pub struct SpecialProcess {
@@ -45,9 +45,7 @@ fn test_session() {
         .unwrap();
 
     let strategy = match strategy {
-        grapl_graph_descriptions::graph_description::id_strategy::Strategy::Session(strategy) => {
-            strategy
-        }
+        rust_proto::graph_descriptions::id_strategy::Strategy::Session(strategy) => strategy,
         _ => panic!("Expected session"),
     };
     assert_eq!(strategy.create_time, 0u64);
diff --git a/src/rust/graph-merger/tests/test_merging.rs b/src/rust/graph-merger/tests/test_merging.rs
index f81bb3d5a4..c1535c17ee 100644
--- a/src/rust/graph-merger/tests/test_merging.rs
+++ b/src/rust/graph-merger/tests/test_merging.rs
@@ -35,7 +35,7 @@ pub mod test {
         Query,
     };
     use graph_merger_lib::upserter::GraphMergeHelper;
-    use grapl_graph_descriptions::*;
+    use rust_proto::graph_descriptions::*;
 
     async fn query_for_uid(dgraph_client: Arc<DgraphClient>, node_key: &str) -> u64 {
         let query_block = QueryBlockBuilder::default()
diff --git a/test/docker-compose.integration-tests.build.yml b/test/docker-compose.integration-tests.build.yml
index b4961ebb96..8f0f595279 100644
--- a/test/docker-compose.integration-tests.build.yml
+++ b/test/docker-compose.integration-tests.build.yml
@@ -11,19 +11,14 @@ services:
       args:
         - RUST_BUILD=test-integration
 
-  analyzerlib-integration-tests:
-    image: ${DOCKER_REGISTRY:-docker.io}/grapl/analyzerlib-test:${TAG:-latest}
+  python-integration-tests:
+    image: ${DOCKER_REGISTRY:-docker.io}/grapl/python-integration-tests:${TAG:-latest}
     build:
       context: ${PWD}
-      dockerfile: ./src/python/Dockerfile
-      target: grapl-analyzerlib-test
+      dockerfile: src/python/Dockerfile
+      target: python-integration-tests
 
-  analyzer-executor-integration-tests:
-    image: ${DOCKER_REGISTRY:-docker.io}/grapl/analyzer-executor-test:${TAG:-latest}
-    build:
-      context: ${PWD}
-      dockerfile: ./src/python/Dockerfile
-      target: analyzer-executor-test
+  # No Docker image for python integration tests - we run them against the Host OS!
 
   # TODO: Re-enable these tests after the following issues are resolved:
   # - https://github.com/grapl-security/issue-tracker/issues/385
@@ -45,10 +40,3 @@ services:
   #     "
   #   environment:
   #     - GRAPL_API_HOST
-
-  graphql-endpoint-tests:
-    image: ${DOCKER_REGISTRY:-docker.io}/grapl/graphql-endpoint-tests:${TAG:-latest}
-    build:
-      context: ${PWD}
-      dockerfile: ./src/python/Dockerfile
-      target: graphql-endpoint-tests
diff --git a/test/docker-compose.typecheck-tests.yml b/test/docker-compose.typecheck-tests.yml
deleted file mode 100644
index 8c482ef635..0000000000
--- a/test/docker-compose.typecheck-tests.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-version: "3.8"
-
-services:
-  typecheck-analyzerlib:
-    image: grapl/analyzerlib-test:${TAG:-latest}
-    build:
-      context: ${PWD}
-      dockerfile: ./src/python/Dockerfile
-      target: grapl-analyzerlib-test
-    command: |
-      /bin/bash -c "
-        cd grapl_analyzerlib &&
-        pip install '.[typecheck]' &&
-        pytype --config ./pytype.cfg .
-        "