about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--users/emery/nix-actor/.gitignore1
-rw-r--r--users/emery/nix-actor/README.md35
-rw-r--r--users/emery/nix-actor/Tupfile6
-rwxr-xr-xusers/emery/nix-actor/Tuprules.jq12
-rw-r--r--users/emery/nix-actor/Tuprules.tup10
-rw-r--r--users/emery/nix-actor/build-nim-sbom.nix195
-rw-r--r--users/emery/nix-actor/default.nix32
-rw-r--r--users/emery/nix-actor/nix_actor.nimble61
-rw-r--r--users/emery/nix-actor/protocol.prs61
-rw-r--r--users/emery/nix-actor/sbom.json596
-rw-r--r--users/emery/nix-actor/service.pr.in13
-rw-r--r--users/emery/nix-actor/src/nix_actor.nim204
-rw-r--r--users/emery/nix-actor/src/nix_actor/Tupfile2
-rw-r--r--users/emery/nix-actor/src/nix_actor/libnix/libexpr.nim106
-rw-r--r--users/emery/nix-actor/src/nix_actor/libnix/main.nim7
-rw-r--r--users/emery/nix-actor/src/nix_actor/libnix/seepuspus.hh21
-rw-r--r--users/emery/nix-actor/src/nix_actor/libnix/stdpuspus.nim33
-rw-r--r--users/emery/nix-actor/src/nix_actor/libnix/store.nim31
-rw-r--r--users/emery/nix-actor/src/nix_actor/nix_api.nim126
-rw-r--r--users/emery/nix-actor/src/nix_actor/nix_api_expr.nim32
-rw-r--r--users/emery/nix-actor/src/nix_actor/nix_api_store.nim38
-rw-r--r--users/emery/nix-actor/src/nix_actor/nix_api_types.nim28
-rw-r--r--users/emery/nix-actor/src/nix_actor/nix_api_util.nim35
-rw-r--r--users/emery/nix-actor/src/nix_actor/nix_api_value.nim94
-rw-r--r--users/emery/nix-actor/src/nix_actor/nix_values.nim235
-rw-r--r--users/emery/nix-actor/src/nix_actor/protocol.nim129
-rw-r--r--users/emery/nix-actor/src/nix_actor/utils.nim26
-rw-r--r--users/emery/nix-actor/tests/Tupfile2
-rw-r--r--users/emery/nix-actor/tests/test.nim143
-rw-r--r--users/emery/nix-actor/tests/test_actor.nim191
-rw-r--r--users/emery/nix-actor/tup.config.nix16
31 files changed, 2521 insertions, 0 deletions
diff --git a/users/emery/nix-actor/.gitignore b/users/emery/nix-actor/.gitignore
new file mode 100644
index 000000000000..8454dc769ad1
--- /dev/null
+++ b/users/emery/nix-actor/.gitignore
@@ -0,0 +1 @@
+/nim.cfg
diff --git a/users/emery/nix-actor/README.md b/users/emery/nix-actor/README.md
new file mode 100644
index 000000000000..6a614c457adc
--- /dev/null
+++ b/users/emery/nix-actor/README.md
@@ -0,0 +1,35 @@
+# Syndicated Nix Actor
+
+An actor for interacting with the [Nix](https://nixos.org/) daemon via the [Syndicated Actor Model](https://syndicate-lang.org/).
+
+See [protocol.prs](./protocol.prs) for the Syndicate protocol [schema](https://preserves.dev/preserves-schema.html).
+
+## Evaluation state as entity capabililties
+
+The actor exposes on its initial capability a gatekeeper that resolves requests in the form `<nix { lookupPath: [ … ], store: … } >`.
+
+The resolved entity is an evaluation state that responds to the assertions `<eval @expr string @args any @result #:Result>` as well as observation of literal values via the dataspace protocol.
+The evaluation state is initialized with the value `nil` and is advanced with Nix functions in the form of `prev: args: body` with a type of `Any -> Any -> Any`.
+
+To evaluate the `hello` package from Nixpkgs one could use an assertion like `<eval "_: pkgName: builtins.getAttr pkgName (import <nixpkgs> {})" "hello" #:…>` which would assert back a new assertion state at `hello`.
+
+The evaluation state represents a lazy Nix expression and must be "realised" to become a physical store path.
+Asserting `<realise-string @result #:Result>` to an evaluation state will return a string with its realized closure at the evaluation store.
+
+With the exception of observations the result value of `<ok @value any>` or `<error @message any>` is used for response assertions.
+
+Dataspace observations work over evaluation state.
+In the example case of an evaluation state positioned at the `hello` package the observation of `{ hello: { meta: { license: { shortName: ? } } } }` would capture the value `"gpl3Plus"`.
+If an attrset contains an `outPath` attribute then the value of `outPath` is captured in place of the attrset.
+This is to avoid traversing deeply nested and recursive Nix values.
+
+## Store replication
+
+Nix stores can be opened using the gatekeeper step `<nix-store { storeUri: "…" }>`.
+The store entity responds to `<check-path @store-path string @result #:any>` with true or false.
+
+To replicate paths between two stores, assert `<replicate @target #:any @storePath string @result #:Result>` to a store entity or a evaluation entity, with `target` set to a store entity or a evaluation entity.
+
+## Worker protocol
+
+The was once an abstraction of the Nix worker socket that could intermediate between clients and the worker but that code has been removed, refer to git history for that.
diff --git a/users/emery/nix-actor/Tupfile b/users/emery/nix-actor/Tupfile
new file mode 100644
index 000000000000..ba7180cce451
--- /dev/null
+++ b/users/emery/nix-actor/Tupfile
@@ -0,0 +1,6 @@
+include_rules
+
+: sbom.json |> !sbom-to-nix |> | ./<lock>
+run ./Tuprules.jq sbom.json
+
+: foreach {bin} |> !assert_built |>
diff --git a/users/emery/nix-actor/Tuprules.jq b/users/emery/nix-actor/Tuprules.jq
new file mode 100755
index 000000000000..2c9a5e70e9f5
--- /dev/null
+++ b/users/emery/nix-actor/Tuprules.jq
@@ -0,0 +1,12 @@
+#! /usr/bin/env -S jq --raw-output --from-file
+.metadata.component.properties as $props |
+$props |
+  ( map( select(.name | .[0:10] == "nim:binDir") ) +
+    map( select(.name | .[0:10] == "nim:srcDir") ) |
+    map( .value )
+  ) + ["."] | .[0] as $binDir |
+
+$props |
+  map( select(.name | .[0:8] == "nim:bin:") ) |
+  map( ": \($binDir)/\(.value).nim |> !nim_bin |> $(BIN_DIR)/\(.name[8:]) {bin}" ) |
+  join("\n")
diff --git a/users/emery/nix-actor/Tuprules.tup b/users/emery/nix-actor/Tuprules.tup
new file mode 100644
index 000000000000..9df8af527b91
--- /dev/null
+++ b/users/emery/nix-actor/Tuprules.tup
@@ -0,0 +1,10 @@
+PROJECT_DIR = $(TUP_CWD)
+include ../syndicate-nim/depends.tup
+
+PKG_CONFIG_PATH += @(PKG_CONFIG_PATH_nix)
+NIM_CFG_LINES += "putenv:PKG_CONFIG=\"@(PKG_CONFIG)\""
+NIM_CFG_LINES += "putenv:PKG_CONFIG_PATH=\"$(PKG_CONFIG_PATH)\""
+NIM_CFG_LINES += "path:\"$(TUP_CWD)/../syndicate-nim/src\""
+
+NIM_GROUPS += $(TUP_CWD)/<lock>
+NIM_GROUPS += $(TUP_CWD)/<protocol>
diff --git a/users/emery/nix-actor/build-nim-sbom.nix b/users/emery/nix-actor/build-nim-sbom.nix
new file mode 100644
index 000000000000..8edf4f98d60f
--- /dev/null
+++ b/users/emery/nix-actor/build-nim-sbom.nix
@@ -0,0 +1,195 @@
+{
+  lib,
+  stdenv,
+  fetchgit,
+  fetchzip,
+  runCommand,
+  xorg,
+  nim,
+  nimOverrides,
+}:
+
+let
+  fetchers = {
+    fetchzip =
+      { url, sha256, ... }:
+      fetchzip {
+        name = "source";
+        inherit url sha256;
+      };
+    fetchgit =
+      {
+        fetchSubmodules ? false,
+        leaveDotGit ? false,
+        rev,
+        sha256,
+        url,
+        ...
+      }:
+      fetchgit {
+        inherit
+          fetchSubmodules
+          leaveDotGit
+          rev
+          sha256
+          url
+          ;
+      };
+  };
+
+  filterPropertiesToAttrs =
+    prefix: properties:
+    lib.pipe properties [
+      (builtins.filter ({ name, ... }: (lib.strings.hasPrefix prefix name)))
+      (map (
+        { name, value }:
+        {
+          name = lib.strings.removePrefix prefix name;
+          inherit value;
+        }
+      ))
+      builtins.listToAttrs
+    ];
+
+  buildNimCfg =
+    { backend, components, ... }:
+    let
+      componentSrcDirs = map (
+        { properties, ... }:
+        let
+          fodProps = filterPropertiesToAttrs "nix:fod:" properties;
+          fod = fetchers.${fodProps.method} fodProps;
+          srcDir = fodProps.srcDir or "";
+        in
+        if srcDir == "" then fod else "${fod}/${srcDir}"
+      ) components;
+    in
+    runCommand "nim.cfg"
+      {
+        outputs = [
+          "out"
+          "src"
+        ];
+        nativeBuildInputs = [ xorg.lndir ];
+      }
+      ''
+        pkgDir=$src/pkg
+        cat << EOF >> $out
+        backend:${backend}
+        path:"$src"
+        path:"$pkgDir"
+        EOF
+        mkdir -p "$pkgDir"
+        ${lib.strings.concatMapStrings (d: ''
+          lndir "${d}" "$pkgDir"
+        '') componentSrcDirs}
+      '';
+
+  buildCommands = lib.attrsets.mapAttrsToList (
+    output: input: ''
+      nim compile $nimFlags --out:${output} ${input}
+    ''
+  );
+
+  installCommands = lib.attrsets.mapAttrsToList (
+    output: input: ''
+      install -Dt $out/bin ${output}
+    ''
+  );
+
+  applySbom =
+    sbom:
+    {
+      passthru ? { },
+      ...
+    }@prevAttrs:
+    let
+      properties = # SBOM metadata.component.properties as an attrset.
+        lib.attrsets.recursiveUpdate (builtins.listToAttrs sbom.metadata.component.properties)
+          passthru.properties or { };
+
+      nimBin = # A mapping of Nim module file paths to names of programs.
+        lib.attrsets.recursiveUpdate (lib.pipe properties [
+          (lib.attrsets.filterAttrs (name: value: lib.strings.hasPrefix "nim:bin:" name))
+          (lib.attrsets.mapAttrs' (
+            name: value: {
+              name = lib.strings.removePrefix "nim:bin:" name;
+              value = "${properties."nim:binDir" or (properties."nim:srcDir" or ".")}/${value}";
+            }
+          ))
+        ]) passthru.nimBin or { };
+    in
+    {
+      strictDeps = true;
+
+      pname = prevAttrs.pname or sbom.metadata.component.name;
+      version = prevAttrs.version or sbom.metadata.component.version or null;
+
+      configurePhase =
+        prevAttrs.configurePhase or ''
+          runHook preConfigure
+          echo "nim.cfg << $nimCfg"
+          cat $nimCfg >> nim.cfg
+          cat << EOF >> nim.cfg
+          nimcache:"$NIX_BUILD_TOP/nimcache"
+          parallelBuild:$NIX_BUILD_CORES
+          EOF
+          runHook postConfigure
+        '';
+
+      buildPhase =
+        prevAttrs.buildPhase or ''
+          runHook preBuild
+          ${lib.strings.concatLines (buildCommands nimBin)}
+          runHook postBuild
+        '';
+
+      installPhase =
+        prevAttrs.installPhase or ''
+          runHook preInstall
+          ${lib.strings.concatLines (installCommands nimBin)}
+          runHook postInstall
+        '';
+
+      nativeBuildInputs = (prevAttrs.nativeBuildInputs or [ ]) ++ [ nim ];
+
+      nimCfg =
+        prevAttrs.nimCfg or (buildNimCfg {
+          backend = prevAttrs.nimBackend or properties."nim:backend" or "c";
+          inherit (sbom) components;
+        });
+
+      passthru = {
+        inherit sbom properties nimBin;
+      };
+    };
+
+  applyOverrides =
+    prevAttrs:
+    builtins.foldl' (
+      prevAttrs:
+      { name, ... }@component:
+      if (builtins.hasAttr name nimOverrides) then
+        let
+          result = nimOverrides.${name} component prevAttrs;
+        in
+        prevAttrs // (if builtins.isAttrs result then result else result { })
+      else
+        prevAttrs
+    ) prevAttrs prevAttrs.passthru.sbom.components;
+
+  compose =
+    callerArg: sbom: finalAttrs:
+    let
+      callerAttrs = if builtins.isAttrs callerArg then callerArg else callerArg finalAttrs;
+      sbomAttrs = callerAttrs // (applySbom sbom callerAttrs);
+      overrideAttrs = sbomAttrs // (applyOverrides sbomAttrs);
+    in
+    overrideAttrs;
+in
+callerArg: sbomArg:
+let
+  sbom = if builtins.isAttrs sbomArg then sbomArg else builtins.fromJSON (builtins.readFile sbomArg);
+  overrideSbom = f: stdenv.mkDerivation (compose callerArg (sbom // (f sbom)));
+in
+(stdenv.mkDerivation (compose callerArg sbom)) // { inherit overrideSbom; }
diff --git a/users/emery/nix-actor/default.nix b/users/emery/nix-actor/default.nix
new file mode 100644
index 000000000000..901496264a3f
--- /dev/null
+++ b/users/emery/nix-actor/default.nix
@@ -0,0 +1,32 @@
+{
+  pkgs ? import <nixpkgs> { },
+  lib ? pkgs.lib,
+  ...
+}:
+
+let
+  buildNimSbom = pkgs.callPackage ./build-nim-sbom.nix { };
+  nix' = pkgs.nixVersions.latest.overrideAttrs (_: {
+    version = "2024-08-23";
+    src = pkgs.fetchFromGitHub {
+      owner = "nixos";
+      repo = "nix";
+      rev = "85f1aa6b3df5c5fcc924a74e2a9cc8acea9ba0e1";
+      hash = "sha256-3+UgAktTtkGUNpxMxr+q+R+z3r026L3PwJzG6RD2IXM=";
+    };
+  });
+in
+buildNimSbom (finalAttrs: {
+  outputs = [
+    "out"
+    "cfg"
+  ];
+  nativeBuildInputs = [ pkgs.pkg-config ];
+  buildInputs = [ nix' ];
+  src = if lib.inNixShell then null else lib.cleanSource ./.;
+  postInstall = ''
+    mkdir $cfg
+    export mainProgram="$out/bin/nix-actor"
+    substituteAll service.pr.in $cfg/service.pr
+  '';
+}) ./sbom.json
diff --git a/users/emery/nix-actor/nix_actor.nimble b/users/emery/nix-actor/nix_actor.nimble
new file mode 100644
index 000000000000..21ac773171f3
--- /dev/null
+++ b/users/emery/nix-actor/nix_actor.nimble
@@ -0,0 +1,61 @@
+# Emulate Nimble from CycloneDX data at sbom.json.
+
+import std/json
+
+proc lookupComponent(sbom: JsonNode; bomRef: string): JsonNode =
+  for c in sbom{"components"}.getElems.items:
+    if c{"bom-ref"}.getStr == bomRef:
+      return c
+  result = newJNull()
+
+let
+  sbom = (getPkgDir() & "/sbom.json").readFile.parseJson
+  comp = sbom{"metadata", "component"}
+  bomRef = comp{"bom-ref"}.getStr
+
+version = comp{"version"}.getStr
+author = comp{"authors"}[0]{"name"}.getStr
+description = comp{"description"}.getStr
+license = comp{"licenses"}[0]{"license", "id"}.getStr
+
+for prop in comp{"properties"}.getElems.items:
+  let (key, val) = (prop{"name"}.getStr, prop{"value"}.getStr)
+  case key
+  of "nim:skipDirs:":
+    add(skipDirs, val)
+  of "nim:skipFiles:":
+    add(skipFiles, val)
+  of "nim:skipExt":
+    add(skipExt, val)
+  of "nim:installDirs":
+    add(installDirs, val)
+  of "nim:installFiles":
+    add(installFiles, val)
+  of "nim:installExt":
+    add(installExt, val)
+  of "nim:binDir":
+    add(binDir, val)
+  of "nim:srcDir":
+    add(srcDir, val)
+  of "nim:backend":
+    add(backend, val)
+  else:
+    if key.startsWith "nim:bin:":
+      namedBin[key[8..key.high]] = val
+
+for depend in sbom{"dependencies"}.items:
+  if depend{"ref"}.getStr == bomRef:
+    for depRef in depend{"dependsOn"}.items:
+      let dep = sbom.lookupComponent(depRef.getStr)
+      var spec = dep{"name"}.getStr
+      for extRef in dep{"externalReferences"}.elems:
+        if extRef{"type"}.getStr == "vcs":
+          spec = extRef{"url"}.getStr
+          break
+      let ver = dep{"version"}.getStr
+      if ver != "":
+        if ver.allCharsInSet {'0'..'9', '.'}: spec.add " == "
+        else: spec.add '#'
+        spec.add ver
+      requires spec
+    break
diff --git a/users/emery/nix-actor/protocol.prs b/users/emery/nix-actor/protocol.prs
new file mode 100644
index 000000000000..c3623234488b
--- /dev/null
+++ b/users/emery/nix-actor/protocol.prs
@@ -0,0 +1,61 @@
+version 1 .
+embeddedType EntityRef.Cap .
+
+# Gatekeeper step to access a Nix store.
+StoreResolveStep = <nix-store @detail StoreResolveDetail> .
+StoreResolveDetail = { storeUri: string }
+& @storeParams StoreParams
+.
+
+# Gatekeeper step to access a Nix evaluator.
+# The resulting capability is a Nix evaluation state with an initial value of <null>.
+EvalResolveStep = <nix @detail EvalResolveDetail> .
+EvalResolveDetail = { }
+& @lookupPath  LookupPath
+& @storeUri    StoreUri
+& @storeParams StoreParams
+.
+
+# List of strings corresponding to entries in NIX_PATH.
+# For example:
+#  [ "nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos"
+#    "nixos-config=/etc/nixos/configuration.nix"
+#    "/nix/var/nix/profiles/per-user/root/channels"
+#  ]
+LookupPath = @lookupPath { lookupPath: [string ...] } / @absent { } .
+
+StoreUri = @storeUri { storeUri: string } / @absent { } .
+
+StoreParams = @storeParams { storeParams: AttrSet } / @absent { } .
+
+# Assertion.
+# Advance a Nix evaluation state.
+# @expr must be a function that takes two parameters,
+# the first is the current evaluation value and the second
+# is @args. A new Nix value capability is return via @result.
+# This capability can be observed using the Dataspace
+# protocol but store paths may not be locally available.
+Eval = <eval @expr string @args any @result #:Result> .
+
+# Assertion.
+# Realise a Nix evaluation state to a string.
+# This makes the closure of store paths referred to by the
+# string present in the evaluation store.
+RealiseString = <realise-string @result #:Result> .
+
+# Assertion.
+# Check at a Nix store if a store path is present and valid.
+CheckStorePath = <check-path @path string @valid #:bool> .
+
+# Assertion.
+# Replicate a store path closure between stores.
+# A Result value is asserted to @result.
+Replicate = <replicate @target #:any @storePath string @result #:Result> .
+
+AttrSet = {symbol: any ...:...} .
+
+# Value.
+Result = Error / <ok @value any>.
+
+# Common error type.
+Error = <error @message any>.
diff --git a/users/emery/nix-actor/sbom.json b/users/emery/nix-actor/sbom.json
new file mode 100644
index 000000000000..567babc1e87d
--- /dev/null
+++ b/users/emery/nix-actor/sbom.json
@@ -0,0 +1,596 @@
+{
+  "bomFormat": "CycloneDX",
+  "specVersion": "1.6",
+  "metadata": {
+    "component": {
+      "type": "application",
+      "bom-ref": "pkg:nim/nix_actor",
+      "name": "nix_actor",
+      "description": "Syndicated Nix Actor",
+      "version": "20240828",
+      "authors": [
+        {
+          "name": "Emery Hemingway"
+        }
+      ],
+      "licenses": [
+        {
+          "license": {
+            "id": "Unlicense"
+          }
+        }
+      ],
+      "properties": [
+        {
+          "name": "nim:skipExt",
+          "value": "nim"
+        },
+        {
+          "name": "nim:bin:nix-actor",
+          "value": "nix_actor"
+        },
+        {
+          "name": "nim:srcDir",
+          "value": "src"
+        },
+        {
+          "name": "nim:backend",
+          "value": "c"
+        }
+      ]
+    }
+  },
+  "components": [
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/syndicate",
+      "name": "syndicate",
+      "version": "20240827",
+      "externalReferences": [
+        {
+          "url": "https://git.syndicate-lang.org/ehmry/syndicate-nim/archive/1a445733561b64de448a9138cf30517df020f9d4.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://git.syndicate-lang.org/ehmry/syndicate-nim",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/mwxdy3sj4jfpaqmvj83rdhij405af1s9-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "1a445733561b64de448a9138cf30517df020f9d4"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "0ghm99jd5nzkrm35ml3w9l95ygd0l8wmpf7d2585j2rg53mfvhn7"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://git.syndicate-lang.org/ehmry/syndicate-nim/archive/1a445733561b64de448a9138cf30517df020f9d4.tar.gz"
+        },
+        {
+          "name": "nix:fod:ref",
+          "value": "20240827"
+        },
+        {
+          "name": "nix:fod:srcDir",
+          "value": "src"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/nimcrypto",
+      "name": "nimcrypto",
+      "version": "485f7b3cfa83c1beecc0e31be0e964d697aa74d7",
+      "externalReferences": [
+        {
+          "url": "https://github.com/cheatfate/nimcrypto/archive/485f7b3cfa83c1beecc0e31be0e964d697aa74d7.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://github.com/cheatfate/nimcrypto",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/fkrcpp8lzj2yi21na79xm63xk0ggnqsp-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "485f7b3cfa83c1beecc0e31be0e964d697aa74d7"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "1h3dzdbc9kacwpi10mj73yjglvn7kbizj1x8qc9099ax091cj5xn"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://github.com/cheatfate/nimcrypto/archive/485f7b3cfa83c1beecc0e31be0e964d697aa74d7.tar.gz"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/preserves",
+      "name": "preserves",
+      "version": "20240823",
+      "externalReferences": [
+        {
+          "url": "https://git.syndicate-lang.org/ehmry/preserves-nim/archive/4fe1952aafad9d852771549669077c0f3577267f.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://git.syndicate-lang.org/ehmry/preserves-nim.git",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/b4cdj9x4y92a9i65j4mqcy89hpkhsjv0-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "4fe1952aafad9d852771549669077c0f3577267f"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "00vs3m7kvz7lxmcz0zj3szqa82gdd3fc8i4qklhwr61cjiinb69w"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://git.syndicate-lang.org/ehmry/preserves-nim/archive/4fe1952aafad9d852771549669077c0f3577267f.tar.gz"
+        },
+        {
+          "name": "nix:fod:ref",
+          "value": "20240823"
+        },
+        {
+          "name": "nix:fod:srcDir",
+          "value": "src"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/sys",
+      "name": "sys",
+      "version": "4ef3b624db86e331ba334e705c1aa235d55b05e1",
+      "externalReferences": [
+        {
+          "url": "https://github.com/ehmry/nim-sys/archive/4ef3b624db86e331ba334e705c1aa235d55b05e1.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://github.com/ehmry/nim-sys.git",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/syhxsjlsdqfap0hk4qp3s6kayk8cqknd-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "4ef3b624db86e331ba334e705c1aa235d55b05e1"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "1q4qgw4an4mmmcbx48l6xk1jig1vc8p9cq9dbx39kpnb0890j32q"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://github.com/ehmry/nim-sys/archive/4ef3b624db86e331ba334e705c1aa235d55b05e1.tar.gz"
+        },
+        {
+          "name": "nix:fod:srcDir",
+          "value": "src"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/taps",
+      "name": "taps",
+      "version": "20240405",
+      "externalReferences": [
+        {
+          "url": "https://git.sr.ht/~ehmry/nim_taps/archive/8c8572cd971d1283e6621006b310993c632da247.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://git.sr.ht/~ehmry/nim_taps",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/6y14ia52kr7jyaa0izx37mlablmq9s65-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "8c8572cd971d1283e6621006b310993c632da247"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "1dp166bv9x773jmfqppg5i3v3rilgff013vb11yzwcid9l7s3iy8"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://git.sr.ht/~ehmry/nim_taps/archive/8c8572cd971d1283e6621006b310993c632da247.tar.gz"
+        },
+        {
+          "name": "nix:fod:ref",
+          "value": "20240405"
+        },
+        {
+          "name": "nix:fod:srcDir",
+          "value": "src"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/coap",
+      "name": "coap",
+      "version": "20230331",
+      "externalReferences": [
+        {
+          "url": "https://codeberg.org/eris/nim-coap/archive/a134213b51a8d250684f2ba26802ffa97fae4ffb.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://codeberg.org/eris/nim-coap.git",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/pqj933cnw7r7hp46jrpjlwh1yr0jvckp-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "a134213b51a8d250684f2ba26802ffa97fae4ffb"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "1wbix6d8l26nj7m3xinh4m2f27n4ma0yzs3x5lpann2ha0y51k8b"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://codeberg.org/eris/nim-coap/archive/a134213b51a8d250684f2ba26802ffa97fae4ffb.tar.gz"
+        },
+        {
+          "name": "nix:fod:ref",
+          "value": "20230331"
+        },
+        {
+          "name": "nix:fod:srcDir",
+          "value": "src"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/npeg",
+      "name": "npeg",
+      "version": "1.2.2",
+      "externalReferences": [
+        {
+          "url": "https://github.com/zevv/npeg/archive/ec0cc6e64ea4c62d2aa382b176a4838474238f8d.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://github.com/zevv/npeg.git",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/xpn694ibgipj8xak3j4bky6b3k0vp7hh-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "ec0cc6e64ea4c62d2aa382b176a4838474238f8d"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "1fi9ls3xl20bmv1ikillxywl96i9al6zmmxrbffx448gbrxs86kg"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://github.com/zevv/npeg/archive/ec0cc6e64ea4c62d2aa382b176a4838474238f8d.tar.gz"
+        },
+        {
+          "name": "nix:fod:ref",
+          "value": "1.2.2"
+        },
+        {
+          "name": "nix:fod:srcDir",
+          "value": "src"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/bigints",
+      "name": "bigints",
+      "version": "20231006",
+      "externalReferences": [
+        {
+          "url": "https://github.com/ehmry/nim-bigints/archive/86ea14d31eea9275e1408ca34e6bfe9c99989a96.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://github.com/ehmry/nim-bigints.git",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/jvrm392g8adfsgf36prgwkbyd7vh5jsw-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "86ea14d31eea9275e1408ca34e6bfe9c99989a96"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "15pcpmnk1bnw3k8769rjzcpg00nahyrypwbxs88jnwr4aczp99j4"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://github.com/ehmry/nim-bigints/archive/86ea14d31eea9275e1408ca34e6bfe9c99989a96.tar.gz"
+        },
+        {
+          "name": "nix:fod:ref",
+          "value": "20231006"
+        },
+        {
+          "name": "nix:fod:srcDir",
+          "value": "src"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/cps",
+      "name": "cps",
+      "version": "0.10.4",
+      "externalReferences": [
+        {
+          "url": "https://github.com/nim-works/cps/archive/2a4d771a715ba45cfba3a82fa625ae7ad6591c8b.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://github.com/nim-works/cps",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/m9vpcf3dq6z2h1xpi1vlw0ycxp91s5p7-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "2a4d771a715ba45cfba3a82fa625ae7ad6591c8b"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "0c62k5wpq9z9mn8cd4rm8jjc4z0xmnak4piyj5dsfbyj6sbdw2bf"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://github.com/nim-works/cps/archive/2a4d771a715ba45cfba3a82fa625ae7ad6591c8b.tar.gz"
+        },
+        {
+          "name": "nix:fod:ref",
+          "value": "0.10.4"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/stew",
+      "name": "stew",
+      "version": "3c91b8694e15137a81ec7db37c6c58194ec94a6a",
+      "externalReferences": [
+        {
+          "url": "https://github.com/status-im/nim-stew/archive/3c91b8694e15137a81ec7db37c6c58194ec94a6a.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://github.com/status-im/nim-stew",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/mqg8qzsbcc8xqabq2yzvlhvcyqypk72c-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "3c91b8694e15137a81ec7db37c6c58194ec94a6a"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "17lfhfxp5nxvld78xa83p258y80ks5jb4n53152cdr57xk86y07w"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://github.com/status-im/nim-stew/archive/3c91b8694e15137a81ec7db37c6c58194ec94a6a.tar.gz"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "bom-ref": "pkg:nim/getdns",
+      "name": "getdns",
+      "version": "20230806",
+      "externalReferences": [
+        {
+          "url": "https://git.sr.ht/~ehmry/getdns-nim/archive/e4ae0992ed7c5540e6d498f3074d06c8f454a0b6.tar.gz",
+          "type": "source-distribution"
+        },
+        {
+          "url": "https://git.sr.ht/~ehmry/getdns-nim",
+          "type": "vcs"
+        }
+      ],
+      "properties": [
+        {
+          "name": "nix:fod:method",
+          "value": "fetchzip"
+        },
+        {
+          "name": "nix:fod:path",
+          "value": "/nix/store/j8i20k9aarzppg4p234449140nnnaycq-source"
+        },
+        {
+          "name": "nix:fod:rev",
+          "value": "e4ae0992ed7c5540e6d498f3074d06c8f454a0b6"
+        },
+        {
+          "name": "nix:fod:sha256",
+          "value": "1dp53gndr6d9s9601dd5ipkiq94j53hlx46mxv8gpr8nd98bqysg"
+        },
+        {
+          "name": "nix:fod:url",
+          "value": "https://git.sr.ht/~ehmry/getdns-nim/archive/e4ae0992ed7c5540e6d498f3074d06c8f454a0b6.tar.gz"
+        },
+        {
+          "name": "nix:fod:ref",
+          "value": "20230806"
+        },
+        {
+          "name": "nix:fod:srcDir",
+          "value": "src"
+        }
+      ]
+    }
+  ],
+  "dependencies": [
+    {
+      "ref": "pkg:nim/nix_actor",
+      "dependsOn": [
+        "pkg:nim/syndicate"
+      ]
+    },
+    {
+      "ref": "pkg:nim/syndicate",
+      "dependsOn": [
+        "pkg:nim/nimcrypto",
+        "pkg:nim/preserves",
+        "pkg:nim/sys",
+        "pkg:nim/taps"
+      ]
+    },
+    {
+      "ref": "pkg:nim/nimcrypto",
+      "dependsOn": []
+    },
+    {
+      "ref": "pkg:nim/preserves",
+      "dependsOn": [
+        "pkg:nim/npeg",
+        "pkg:nim/bigints"
+      ]
+    },
+    {
+      "ref": "pkg:nim/sys",
+      "dependsOn": [
+        "pkg:nim/cps",
+        "pkg:nim/stew"
+      ]
+    },
+    {
+      "ref": "pkg:nim/taps",
+      "dependsOn": [
+        "pkg:nim/getdns",
+        "pkg:nim/sys",
+        "pkg:nim/cps"
+      ]
+    },
+    {
+      "ref": "pkg:nim/coap",
+      "dependsOn": [
+        "pkg:nim/taps"
+      ]
+    },
+    {
+      "ref": "pkg:nim/npeg",
+      "dependsOn": []
+    },
+    {
+      "ref": "pkg:nim/bigints",
+      "dependsOn": []
+    },
+    {
+      "ref": "pkg:nim/cps",
+      "dependsOn": []
+    },
+    {
+      "ref": "pkg:nim/stew",
+      "dependsOn": []
+    },
+    {
+      "ref": "pkg:nim/getdns",
+      "dependsOn": []
+    }
+  ]
+}
diff --git a/users/emery/nix-actor/service.pr.in b/users/emery/nix-actor/service.pr.in
new file mode 100644
index 000000000000..b4a660d4ced9
--- /dev/null
+++ b/users/emery/nix-actor/service.pr.in
@@ -0,0 +1,13 @@
+<daemon @name@ {
+  argv: [ "@mainProgram@" ]
+  clearEnv: #t
+  protocol: application/syndicate
+}>
+
+? <q <service nix ?detail>> [
+  <require-service <daemon @name@>>
+  ? <service-object <daemon @name@> ?obj> [
+    let ?rewriter = <* $config [<rewrite ?resp <a <service nix $detail> $resp>>]>
+    $obj <resolve <nix-actor $detail> $rewriter>
+  ]
+]
diff --git a/users/emery/nix-actor/src/nix_actor.nim b/users/emery/nix-actor/src/nix_actor.nim
new file mode 100644
index 000000000000..8905330a8dd9
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor.nim
@@ -0,0 +1,204 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+import
+  std/[options, strutils, tables],
+  pkg/preserves,
+  pkg/preserves/sugar,
+  pkg/syndicate,
+  pkg/syndicate/[gatekeepers, patterns, relays],
+  ./nix_actor/[nix_api, nix_values],
+  ./nix_actor/protocol
+
+proc echo(args: varargs[string, `$`]) {.used.} =
+  stderr.writeLine(args)
+
+type Value = preserves.Value
+
+template tryPublish(turn: Turn, cap: Cap; body: untyped) =
+  try: body
+  except CatchableError as err:
+    when not defined(release):
+      stderr.writeLine err.msg
+    publish(turn, cap, Error(message: %err.msg))
+
+proc publishOk(turn: Turn; cap: Cap, v: Value) =
+  publish(turn, cap, protocol.ResultOk(value: v))
+
+proc publishError(turn: Turn; cap: Cap, v: Value) =
+  publish(turn, cap, protocol.Error(message: v))
+
+proc fromEmbedded[E](entity: var E; emb: EmbeddedRef): bool =
+  if emb of Cap and emb.Cap.target of E:
+    entity = emb.Cap.target.E
+    result = true
+
+proc unembedEntity(emb: EmbeddedRef; E: typedesc): Option[E] =
+  if emb of Cap and emb.Cap.target of E:
+    result = emb.Cap.target.E.some
+
+proc unembedEntity(v: Value; E: typedesc): Option[E] =
+  if v.isEmbeddedRef:
+    result = v.embeddedRef.unembedEntity(E)
+
+proc openStore(uri: string; params: Option[AttrSet]): Store =
+  var pairs: seq[string]
+  if params.isSome:
+    var  i: int
+    pairs.setLen(params.get.len)
+    for (key, val) in params.get.pairs:
+      pairs[i] = $key & "=" & $val
+      inc i
+  openStore(uri, pairs)
+
+type
+  NixEntity = ref object of Entity
+    self: Cap
+    store: Store
+
+  StoreEntity {.final.} = ref object of NixEntity
+
+  EvalEntity {.final.} = ref object of NixEntity
+    state: EvalState
+    root: NixValue
+
+proc newStoreEntity(turn: Turn; detail: StoreResolveDetail): StoreEntity =
+  let entity = StoreEntity(store: openStore(detail.storeUri, detail.storeParams))
+  entity.self = turn.newCap(entity)
+  entity.self.relay.onStop do (turn: Turn):
+    entity.store.close()
+  entity
+
+proc serve(entity: StoreEntity; turn: Turn; checkPath: CheckStorePath) =
+  tryPublish(turn, checkPath.valid.Cap):
+    var v = entity.store.isValidPath(checkPath.path)
+    publish(turn, checkPath.valid.Cap, initRecord("ok", %v))
+
+proc serve(entity: NixEntity; turn: Turn; rep: Replicate) =
+  tryPublish(turn, rep.result.Cap):
+    var
+      target: Store
+      otherEntity = rep.target.unembedEntity(NixEntity)
+    if otherEntity.isSome:
+      target = otherEntity.get.store
+    if target.isNil:
+      publishError(turn, rep.result.Cap, %"cannot replicate with target")
+    else:
+      if entity.store.isValidPath(rep.storePath):
+        entity.store.copyClosure(target, rep.storePath)
+          # path exists at entity
+      else:
+        target.copyClosure(entity.store, rep.storePath)
+          # path hopefully exists at target
+      publishOk(turn, rep.result.Cap, %rep.storePath)
+
+method publish(entity: StoreEntity; turn: Turn; a: AssertionRef; h: Handle) =
+  var
+    # TODO: this would be a union object
+    # but orc doesn't support it yet.
+    checkStorePath: CheckStorePath
+    replicate: Replicate
+  if checkStorePath.fromPreserves(a.value):
+    entity.serve(turn, checkStorePath)
+  elif replicate.fromPreserves(a.value):
+    entity.serve(turn, replicate)
+
+proc newEvalEntity(turn: Turn; detail: EvalResolveDetail): EvalEntity =
+  ## Create an initial evaluation state.
+  let entity = EvalEntity(
+      store: openStore(detail.storeUri.get("auto"), detail.storeParams)
+    )
+  if detail.lookupPath.isSome:
+    entity.state = newState(entity.store, detail.lookupPath.get)
+  else:
+    entity.state = newState(entity.store)
+  entity.root = entity.state.initNull()
+  entity.self = turn.newCap(entity)
+  entity.self.relay.onStop do (turn: Turn):
+    decref(entity.root)
+    entity.state.close()
+    entity.store.close()
+  entity
+
+proc newChild(parent: EvalEntity; turn: Turn; val: NixValue): EvalEntity =
+  ## Create a child entity for a given root value.
+  let entity = EvalEntity(
+      store: parent.store,
+      state: parent.state,
+      root: val
+    )
+  turn.inFacet do (turn: Turn):
+    entity.facet = turn.facet
+    entity.self = newCap(turn, entity)
+    entity.self.relay.onStop do (turn: Turn):
+      decref(entity.root)
+  entity
+
+proc serve(entity: EvalEntity; turn: Turn; obs: Observe) =
+  ## Dataspace emulation.
+  let facet = turn.facet
+  var
+    analysis = analyse(obs.pattern)
+    captures = newSeq[Value](analysis.capturePaths.len)
+  block stepping:
+    for i, path in analysis.constPaths:
+      var v = entity.state.step(entity.root, path)
+      if v.isNone or v.get != analysis.constValues[i]:
+        let null = initRecord("null")
+        for v in captures.mitems: v = null
+        break stepping
+    for i, path in analysis.capturePaths:
+      var v = entity.state.step(entity.root, path)
+      if v.isSome:
+        captures[i] = v.get.unthunkAll
+      else:
+        captures[i] = initRecord("null")
+  publish(turn, Cap obs.observer, captures)
+
+proc serve(entity: EvalEntity; turn: Turn; r: RealiseString) =
+  tryPublish(turn, r.result.Cap):
+    var str = entity.state.realiseString(entity.root)
+    publishOk(turn, r.result.Cap, %str)
+
+proc serve(entity: EvalEntity; turn: Turn; e: Eval) =
+  tryPublish(turn, e.result.Cap):
+    var expr = entity.state.evalFromString(e.expr)
+    expr = entity.state.apply(expr, entity.root)
+    expr = entity.state.apply(expr, e.args.toNix(entity.state))
+    publishOk(turn, e.result.Cap, entity.newChild(turn, expr).self.toPreserves)
+
+method publish(entity: EvalEntity; turn: Turn; a: AssertionRef; h: Handle) =
+  var
+    # TODO: this would be a union object
+    # but orc doesn't support it yet.
+    eval: Eval
+    observe: Observe
+    realise: RealiseString
+    replicate: Replicate
+  if observe.fromPreserves(a.value) and observe.observer of Cap:
+    serve(entity, turn, observe)
+  elif realise.fromPreserves(a.value) and realise.result of Cap:
+    serve(entity, turn, realise)
+  elif eval.fromPreserves(a.value) and eval.result of Cap:
+    serve(entity, turn, eval)
+  elif replicate.fromPreserves(a.value) and replicate.result of Cap:
+    serve(entity, turn, replicate)
+  else:
+    when not defined(release):
+      echo "unhandled assertion ", a.value
+
+proc bootActor*(turn: Turn; relay: Cap) =
+  initLibstore()
+  initLibexpr()
+
+  let gk = spawnGatekeeper(turn, relay)
+
+  gk.serve do (turn: Turn; step: StoreResolveStep) -> rpc.Result:
+    newStoreEntity(turn, step.detail).self.resultOk
+
+  gk.serve do (turn: Turn; step: EvalResolveStep) -> rpc.Result:
+    newEvalEntity(turn, step.detail).self.resultOk
+
+when isMainModule:
+  runActor("main") do (turn: Turn):
+    resolveEnvironment(turn, bootActor)
diff --git a/users/emery/nix-actor/src/nix_actor/Tupfile b/users/emery/nix-actor/src/nix_actor/Tupfile
new file mode 100644
index 000000000000..81799a46b0a8
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/Tupfile
@@ -0,0 +1,2 @@
+include_rules
+: $(PROJECT_DIR)/protocol.prs |> !preserves-schema-nim |> protocol.nim | $(PROJECT_DIR)/<protocol>
diff --git a/users/emery/nix-actor/src/nix_actor/libnix/libexpr.nim b/users/emery/nix-actor/src/nix_actor/libnix/libexpr.nim
new file mode 100644
index 000000000000..cd5722bb0505
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/libnix/libexpr.nim
@@ -0,0 +1,106 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+import ./stdpuspus, ./store
+
+{.passC: staticExec"$PKG_CONFIG --cflags nix-expr".}
+{.passL: staticExec"$PKG_CONFIG --libs nix-expr".}
+
+proc parentDir(path: string): string =
+  var i = path.high
+  while path[i] != '/': dec(i)
+  path[0..i]
+
+{.passC: "-I" & parentDir(currentSourcePath).}
+
+type
+  NixInt* = int64
+  NixFloat* = float64
+
+  ValueKind* {.importcpp: "nix::ValueType", header: "value.hh".} = enum
+    nThunk,
+    nInt,
+    nFloat,
+    nBool,
+    nString,
+    nPath,
+    nNull,
+    nAttrs,
+    nList,
+    nFunction,
+    nExternal,
+  Value* = ValueObj | ValuePtr
+  ValuePtr* = ptr ValueObj
+  ValueObj* {.importcpp: "nix::Value", header: "value.hh".} = object
+    integer*: NixInt
+    boolean*: bool
+    string: StringContext
+    path*: cstring
+    fpoint*: NixFloat
+    attrs: Bindings
+  StringContext = object
+    s: cstring
+  Symbol* {.importcpp: "nix::Symbol", header: "symbol-table.hh".} = object
+    discard
+  Attr {.importcpp: "nix::Attr", header: "attr-set.hh".} = object
+    name: Symbol
+    value: ValuePtr
+  Bindings = ptr BindginsObj
+  BindginsObj {.importcpp: "nix::Bindings", header: "attr-set.hh".} = object
+    discard
+
+proc kind*(val: Value): ValueKind {.importcpp: "#.type()".}
+
+proc showType*(val: Value): StdString {.importcpp.}
+
+proc shallowString*(val: Value): string =
+  if val.kind != nString:
+    raise newException(FieldDefect, "Value not an attribute set")
+  $val.string.s
+
+proc size(bindings: Bindings): csize_t {.importcpp.}
+
+proc `[]`(b: Bindings; i: Natural): Attr {.importcpp: "(*#)[#]".}
+
+iterator pairs*(val: Value): (Symbol, ValuePtr) =
+  if val.kind != nAttrs:
+    raise newException(FieldDefect, "Value not an attribute set")
+  for i in 0..<val.attrs.size():
+    let attr = val.attrs[i]
+    yield (attr.name, attr.value)
+
+proc listSize(val: Value): csize_t {.importcpp.}
+
+proc listElems(val: Value): ptr UncheckedArray[ValuePtr] {.importcpp.}
+
+iterator items*(val: Value): ValuePtr =
+  if val.kind != nList:
+    raise newException(FieldDefect, "Value not a list")
+  for i in 0..<val.listSize:
+    yield val.listElems()[i]
+
+type
+  ExprObj {.importcpp: "nix::Expr", header: "nixexpr.hh".} = object
+    discard
+  Expr* = ptr ExprObj
+  EvalState* {.importcpp: "std::shared_ptr<nix::EvalState>", header: "eval.hh".} = object
+    discard
+
+proc newEvalState*(store: Store): EvalState {.
+  importcpp: "nix::newEvalState(@)", header: "seepuspus.hh", constructor.}
+
+proc parseExprFromString*(state: EvalState; s, basePath: cstring): Expr {.
+  importcpp: "#->parseExprFromString(@)".}
+
+proc eval*(state: EvalState; expr: Expr; value: var ValueObj) {.
+  importcpp: "#->eval(@)".}
+
+proc forceValueDeep*(state: EvalState; value: var ValueObj) {.
+  importcpp: "#->forceValueDeep(@)".}
+
+proc stringView(state: EvalState; sym: Symbol): StringView {.
+  importcpp: "((std::string_view)#->symbols[#])".}
+
+proc symbolString*(state: EvalState; sym: Symbol): string = $stringView(state, sym)
+
+proc initGC*() {.importcpp: "nix::initGC", header: "eval.hh".}
diff --git a/users/emery/nix-actor/src/nix_actor/libnix/main.nim b/users/emery/nix-actor/src/nix_actor/libnix/main.nim
new file mode 100644
index 000000000000..0d38d41dab59
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/libnix/main.nim
@@ -0,0 +1,7 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+{.passC: staticExec"$PKG_CONFIG --cflags nix-main".}
+{.passL: staticExec"$PKG_CONFIG --libs nix-main".}
+
+proc initNix*() {.importcpp: "nix::initNix", header: "shared.hh".}
diff --git a/users/emery/nix-actor/src/nix_actor/libnix/seepuspus.hh b/users/emery/nix-actor/src/nix_actor/libnix/seepuspus.hh
new file mode 100644
index 000000000000..f1ca69484fdf
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/libnix/seepuspus.hh
@@ -0,0 +1,21 @@
+#pragma once
+#include "eval.hh"
+
+namespace nix {
+
+  std::shared_ptr<nix::EvalState> newEvalState(ref<Store> store)
+  {
+    auto searchPath = Strings();
+    auto evalState =
+      #if HAVE_BOEHMGC
+        std::allocate_shared<EvalState>(
+          traceable_allocator<EvalState>(), searchPath, store, store)
+      #else
+        std::make_shared<EvalState>(
+          searchPath, store, store)
+      #endif
+        ;
+    return evalState;
+  }
+
+}
diff --git a/users/emery/nix-actor/src/nix_actor/libnix/stdpuspus.nim b/users/emery/nix-actor/src/nix_actor/libnix/stdpuspus.nim
new file mode 100644
index 000000000000..719f2f43d3d3
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/libnix/stdpuspus.nim
@@ -0,0 +1,33 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+type StdException* {.importcpp: "std::exception", header: "<exception>".} = object
+
+proc what*(ex: StdException): cstring {.importcpp: "((char *)#.what())", nodecl.}
+
+
+type StdString* {.importcpp: "std::string", header: "<string>".} = object
+
+proc c_str*(s: StdString): cstring {.importcpp.}
+
+type StringView* {.importcpp: "std::string_view", header: "<string>".} = object
+
+proc toStringView*(s: pointer; count: int): StringView {.
+  importcpp: "std::string_view(static_cast<const char *>(#), #)", constructor.}
+
+proc toStringView*(s: string): StringView {.inline.} =
+  if s.len == 0: toStringView(nil, 0)
+  else: toStringView(unsafeAddr s[0], s.len)
+
+proc toStringView*(buf: openarray[byte]): StringView {.inline.} =
+  if buf.len == 0: toStringView(nil, 0)
+  else: toStringView(unsafeAddr buf[0], buf.len)
+
+proc toStringView*(sv: StringView): StringView {.inline.} = sv
+
+proc data(sv: StringView): pointer {.importcpp.}
+proc size(sv: StringView): csize_t {.importcpp.}
+
+proc `$`*(sv: StringView): string =
+  result = newString(sv.size)
+  copyMem(addr result[0], sv.data, result.len)
diff --git a/users/emery/nix-actor/src/nix_actor/libnix/store.nim b/users/emery/nix-actor/src/nix_actor/libnix/store.nim
new file mode 100644
index 000000000000..b8b890397a7b
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/libnix/store.nim
@@ -0,0 +1,31 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+{.passC: staticExec"$PKG_CONFIG --cflags nix-store".}
+{.passL: staticExec"$PKG_CONFIG --libs nix-store".}
+
+{.passC: "'-DSYSTEM=\"x86_64-linux\"'".}
+
+type StdString {.importcpp: "std::string", header: "<string>".} = object
+proc data(s: StdString): pointer {.importcpp: "#.data()".}
+proc len(s: StdString): csize_t {.importcpp: "#.length()".}
+proc `$`*(cpp: StdString): string =
+  result.setLen(cpp.len)
+  if result.len > 0:
+    copyMem(addr result[0], cpp.data, result.len)
+
+type
+  StorePath {.importcpp: "nix::StorePath", header: "path.hh".} = object
+    discard
+
+var nixVersion* {.importc: "nix::nixVersion", header: "globals.hh".}: StdString
+
+proc isDerivation*(path: StorePath): bool {.importcpp.}
+
+type
+  Store* {.importcpp: "nix::ref<nix::Store>", header: "store-api.hh".} = object
+    discard
+
+proc ensurePath*(store: Store; path: StorePath) {.importcpp.}
+
+proc openStore*(): Store {.importcpp: "nix::openStore".}
diff --git a/users/emery/nix-actor/src/nix_actor/nix_api.nim b/users/emery/nix-actor/src/nix_actor/nix_api.nim
new file mode 100644
index 000000000000..6550e589b2b7
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/nix_api.nim
@@ -0,0 +1,126 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+import
+  ./nix_api_expr,
+  ./nix_api_store,
+  ./nix_api_types,
+  ./nix_api_value,
+  ./utils
+
+export NixContext, Store, EvalState, Value, ValueType,
+  gc_decref, isNil
+
+type
+  StringCallback = proc (s: string) {.closure.}
+  StringCallbackState = object
+    callback: StringCallback
+
+proc receiveString(start: cstring; n: cuint; state: pointer) {.cdecl.} =
+  let state = cast[ptr StringCallbackState](state)
+  assert not state.isNil
+  var buf = newString(n)
+  if n > 0:
+    copyMem(buf[0].addr, start, buf.len)
+  state.callback(buf)
+
+proc initLibstore*() =
+  mitNix:
+    checkError nix.libstore_init()
+
+proc initLibexpr*() =
+  mitNix:
+    checkError nix.libexpr_init()
+
+proc openStore*(uri = "auto", params: openarray[string] = []): Store =
+  mitNix:
+    if params.len == 0:
+      result = nix.store_open(uri, nil)
+    else:
+      var args = allocCStringArray(params)
+      defer: deallocCStringArray(args)
+      result = nix.store_open(uri, addr args)
+  assert not result.isNil
+
+proc close*(store: Store) = store_free(store)
+
+proc getUri*(store: Store; cb: StringCallback) =
+  mitNix:
+    let state = new StringCallbackState
+    state.callback = cb
+    checkError nix.store_get_uri(store, receiveString, state[].addr)
+
+proc getVersion*(store: Store; cb: StringCallback) =
+  mitNix:
+    let state = new StringCallbackState
+    state.callback = cb
+    checkError nix.store_get_version(store, receiveString, state[].addr)
+
+proc isValidPath*(store: Store; path: string): bool =
+  assert not store.isNil
+  assert path != ""
+  mitNix:
+    assert not nix.isNil
+    let sp = nix.store_parse_path(store, path)
+    if sp.isNil:
+      raise newException(CatchableError, "store_parse_path failed")
+    defer: store_path_free(sp)
+    result = nix.store_is_valid_path(store, sp)
+
+proc copyClosure*(src, dst: Store; path: string) =
+  assert path != ""
+  mitNix:
+    let sp = nix.store_parse_path(src, path)
+    if sp.isNil:
+      raise newException(CatchableError, "store_parse_path failed")
+    defer: store_path_free(sp)
+    checkError nix.store_copy_closure(src, dst, sp)
+
+proc newState*(store: Store; lookupPath: openarray[string] = []): EvalState =
+  mitNix:
+    var path = allocCStringArray(lookupPath)
+    defer: deallocCStringArray(path)
+    result = nix.state_create(path, store)
+  assert not result.isNil
+
+proc close*(state: EvalState) = state_free(state)
+
+proc close*(value: Value) =
+  mitNix:
+    checkError nix.gc_decref(cast[pointer](value))
+
+proc evalFromString*(nix: NixContext; state: EvalState; expr, path: string; result: Value)  =
+  checkError nix.expr_eval_from_string(state, expr, path, result)
+
+proc evalFromString*(state: EvalState; expr: string; path = "."): Value =
+  mitNix:
+    result = nix.alloc_value(state)
+    nix.evalFromString(state, expr, path, result)
+
+proc force*(state: EvalState; value: Value) =
+  mitNix:
+    checkError nix.value_force(state, value)
+
+proc get_attr_byidx*(value: Value; state: EvalState; i: cuint): (cstring, Value) =
+  mitNix:
+    result[1] = nix.get_attr_byidx(value, state, i, addr result[0])
+
+proc apply(nix: NixContext; state: EvalState; fn, arg: Value): Value =
+  result = nix.alloc_value(state)
+  checkError nix.init_apply(result, fn, arg)
+
+proc apply*(state: EvalState; fn, arg: Value): Value =
+  mitNix:
+    result = nix.apply(state, fn, arg)
+
+proc call*(state: EvalState; fn: Value; args: varargs[Value]): Value =
+  mitNix:
+    result = nix.alloc_value(state)
+    var array = cast[ptr UncheckedArray[Value]](args)
+    checkError nix.value_call_multi(state, fn, args.len.csize_t, array, result)
+
+proc incref*(v: Value) =
+  mitNix: checkError nix.gc_incref(cast[pointer](v))
+
+proc decref*(v: Value) =
+  mitNix: checkError nix.gc_decref(cast[pointer](v))
diff --git a/users/emery/nix-actor/src/nix_actor/nix_api_expr.nim b/users/emery/nix-actor/src/nix_actor/nix_api_expr.nim
new file mode 100644
index 000000000000..748933ad46fe
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/nix_api_expr.nim
@@ -0,0 +1,32 @@
+## Module generated by c2nim from nix_api_expr.h
+
+import ./nix_api_types
+
+{.passC: staticExec"$PKG_CONFIG --cflags nix-expr-c".}
+{.passL: staticExec"$PKG_CONFIG --libs nix-expr-c".}
+
+{.pragma: nix_api_expr, header: "nix_api_expr.h", importc: "nix_$1".}
+
+proc libexpr_init*(context: NixContext): nix_err {.nix_api_expr.}
+
+proc expr_eval_from_string*(context: NixContext; state: EvalState; expr: cstring; path: cstring; value: Value): nix_err {.nix_api_expr.}
+
+proc value_call*(context: NixContext; state: EvalState; fn: Value; arg: Value; value: Value): nix_err {.nix_api_expr.}
+
+proc value_call_multi*(context: NixContext; state: EvalState; fn: Value; nargs: csize_t; args: ptr UncheckedArray[Value]; value: Value): nix_err {.nix_api_expr.}
+
+proc value_force*(context: NixContext; state: EvalState; value: Value): nix_err {.nix_api_expr.}
+
+proc value_force_deep*(context: NixContext; state: EvalState; value: Value): nix_err {.nix_api_expr.}
+
+proc state_create*(context: NixContext; lookupPath: cstringArray; store: Store): EvalState {.nix_api_expr.}
+
+proc state_free*(state: EvalState) {.nix_api_expr.}
+
+proc gc_incref*(context: NixContext; `object`: pointer): nix_err {.nix_api_expr.}
+
+proc gc_decref*(context: NixContext; `object`: pointer|Value): nix_err {.nix_api_expr.}
+
+proc gc_now*() {.nix_api_expr.}
+
+proc gc_register_finalizer*(obj: pointer; cd: pointer; finalizer: proc (obj: pointer; cd: pointer)) {.nix_api_expr.}
diff --git a/users/emery/nix-actor/src/nix_actor/nix_api_store.nim b/users/emery/nix-actor/src/nix_actor/nix_api_store.nim
new file mode 100644
index 000000000000..2df3f891ede4
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/nix_api_store.nim
@@ -0,0 +1,38 @@
+## Module generated by c2nim for nix_api_store.h
+
+import ./nix_api_types
+
+{.passC: staticExec"$PKG_CONFIG --cflags nix-store-c".}
+{.passL: staticExec"$PKG_CONFIG --libs nix-store-c".}
+
+{.pragma: nix_api_store, header: "nix_api_store.h", importc: "nix_$1".}
+
+proc libstore_init*(context: NixContext): nix_err {.nix_api_store.}
+
+proc libstore_init_no_load_config*(context: NixContext): nix_err {.nix_api_store.}
+
+proc init_plugins*(context: NixContext): nix_err {.nix_api_store.}
+
+proc store_open*(context: NixContext; uri: cstring; params: ptr cstringArray): Store {.nix_api_store.}
+
+proc store_free*(store: Store) {.nix_api_store.}
+
+proc store_get_uri*(context: NixContext; store: Store; callback: GetStringCallback; user_data: pointer): nix_err {.nix_api_store.}
+
+proc store_parse_path*(context: NixContext; store: Store; path: cstring): StorePath {.nix_api_store.}
+
+proc store_path_name*(store_path: StorePath; callback: GetStringCallback; user_data: pointer) {.nix_api_store.}
+
+proc store_path_clone*(p: StorePath): StorePath {.nix_api_store.}
+
+proc store_path_free*(p: StorePath) {.nix_api_store.}
+
+proc store_is_valid_path*(context: NixContext; store: Store; path: StorePath): bool {.nix_api_store.}
+
+type RealiseCallback* = proc (userdata: pointer; outname: cstring; `out`: cstring) {.cdecl.}
+
+proc store_realise*(context: NixContext; store: Store; path: StorePath; userdata: pointer; callback: RealiseCallback): nix_err {.nix_api_store.}
+
+proc store_get_version*(context: NixContext; store: Store; callback: GetStringCallback; user_data: pointer): nix_err {.nix_api_store.}
+
+proc store_copy_closure*(context: NixContext; src, dst: Store; path: StorePath): nix_err {.nix_api_store.}
diff --git a/users/emery/nix-actor/src/nix_actor/nix_api_types.nim b/users/emery/nix-actor/src/nix_actor/nix_api_types.nim
new file mode 100644
index 000000000000..9623ba5e075b
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/nix_api_types.nim
@@ -0,0 +1,28 @@
+type
+  nix_err* = cint
+  EvalState* {.header: "nix_api_expr.h", importc.} = distinct pointer
+  NixContext* {.header: "nix_api_util.h", importc: "nix_c_context".} = distinct pointer
+  NixException* = object of CatchableError
+  Store* {.header: "nix_api_store.h", importc.} = distinct pointer
+  StorePath* {.header: "nix_api_store.h", importc.} = distinct pointer
+  Value* {.header: "nix_api_value.h", importc.} = distinct pointer
+  ValueType* {.header: "nix_api_value.h", importc.} = enum
+    NIX_TYPE_THUNK,
+    NIX_TYPE_INT,
+    NIX_TYPE_FLOAT,
+    NIX_TYPE_BOOL,
+    NIX_TYPE_STRING,
+    NIX_TYPE_PATH,
+    NIX_TYPE_NULL,
+    NIX_TYPE_ATTRS,
+    NIX_TYPE_LIST,
+    NIX_TYPE_FUNCTION,
+    NIX_TYPE_EXTERNAL
+
+  GetStringCallback* = proc (start: cstring; n: cuint; data: pointer) {.cdecl.}
+
+proc isNil*(p: EvalState): bool {.borrow.}
+proc isNil*(p: NixContext): bool {.borrow.}
+proc isNil*(p: Store): bool {.borrow.}
+proc isNil*(p: StorePath): bool {.borrow.}
+proc isNil*(p: Value): bool {.borrow.}
diff --git a/users/emery/nix-actor/src/nix_actor/nix_api_util.nim b/users/emery/nix-actor/src/nix_actor/nix_api_util.nim
new file mode 100644
index 000000000000..2a78428707a4
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/nix_api_util.nim
@@ -0,0 +1,35 @@
+## Module generated by c2nim for nix_api_util.h
+
+import ./nix_api_types
+
+{.pragma: nix_api_util, header: "nix_api_util.h", importc: "nix_$1".}
+{.pragma: importUtil, header: "nix_api_util.h", importc.}
+
+var
+  NIX_OK* {.importUtil.}: cint
+  NIX_ERR_UNKNOWN* {.importUtil.}: cint
+  NIX_ERR_OVERFLOW* {.importUtil.}: cint
+  NIX_ERR_KEY* {.importUtil.}: cint
+  NIX_ERR_NIX_ERROR* {.importUtil.}: cint
+
+proc c_context_create*(): NixContext {.nix_api_util.}
+
+proc c_context_free*(context: NixContext) {.nix_api_util.}
+
+proc libutil_init*(context: NixContext): nix_err {.nix_api_util.}
+
+proc setting_get*(context: NixContext; key: cstring; value: cstring; n: cint): nix_err {.nix_api_util.}
+
+proc setting_set*(context: NixContext; key: cstring; value: cstring): nix_err {.nix_api_util.}
+
+proc version_get*(): cstring {.nix_api_util.}
+
+proc err_msg*(context: NixContext; ctx: NixContext; n: ptr cuint): cstring {.nix_api_util.}
+
+proc err_info_msg*(context: NixContext; read_context: NixContext; value: cstring; n: cint): nix_err {.nix_api_util.}
+
+proc err_name*(context: NixContext; read_context: NixContext; value: cstring; n: cint): nix_err {.nix_api_util.}
+
+proc err_code*(read_context: NixContext): nix_err {.nix_api_util.}
+
+proc set_err_msg*(context: NixContext; err: nix_err; msg: cstring): nix_err {.nix_api_util.}
diff --git a/users/emery/nix-actor/src/nix_actor/nix_api_value.nim b/users/emery/nix-actor/src/nix_actor/nix_api_value.nim
new file mode 100644
index 000000000000..6c61f8883534
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/nix_api_value.nim
@@ -0,0 +1,94 @@
+## Module generated by c2nim for nix_api_value.h
+
+import ./nix_api_types
+
+{.pragma: nix_api_value, header: "nix_api_value.h", importc: "nix_$1".}
+
+type
+  BindingsBuilder* {.header: "nix_api_value.h", importc.} = distinct pointer
+  ExternalValue* {.header: "nix_api_value.h", importc.} = distinct pointer
+  ListBuilder* {.header: "nix_api_value.h", importc.} = distinct pointer
+  RealisedString* {.header: "nix_api_value.h", importc: "nix_realised_string".} = distinct pointer
+
+proc isNil*(p: BindingsBuilder|ExternalValue|ListBuilder|RealisedString): bool =
+  cast[pointer](p).isNil
+
+proc alloc_value*(context: NixContext; state: EvalState): Value {.nix_api_value.}
+
+proc get_type*(context: NixContext; value: Value): ValueType {.nix_api_value.}
+
+proc get_typename*(context: NixContext; value: Value): cstring {.nix_api_value.}
+
+proc get_bool*(context: NixContext; value: Value): bool {.nix_api_value.}
+
+proc get_string*(context: NixContext; value: Value; callback: GetStringCallback; user_data: pointer): nix_err {.nix_api_value.}
+
+proc get_path_string*(context: NixContext; value: Value): cstring {.nix_api_value.}
+
+proc get_list_size*(context: NixContext; value: Value): cuint {.nix_api_value.}
+
+proc get_attrs_size*(context: NixContext; value: Value): cuint {.nix_api_value.}
+
+proc get_float*(context: NixContext; value: Value): cdouble {.nix_api_value.}
+
+proc get_int*(context: NixContext; value: Value): int64 {.nix_api_value.}
+
+proc get_external*(context: NixContext; a2: Value): ExternalValue {.nix_api_value.}
+
+proc get_list_byidx*(context: NixContext; value: Value; state: EvalState; ix: cuint): Value {.nix_api_value.}
+
+proc get_attr_byname*(context: NixContext; value: Value; state: EvalState; name: cstring): Value {.nix_api_value.}
+
+proc has_attr_byname*(context: NixContext; value: Value; state: EvalState; name: cstring): bool {.nix_api_value.}
+
+proc get_attr_byidx*(context: NixContext; value: Value; state: EvalState; i: cuint; name: ptr cstring): Value {.nix_api_value.}
+
+# proc get_attr_name_byidx*(context: NixContext; value: Value; state: EvalState; i: cuint): cstring {.nix_api_value.}
+
+proc init_bool*(context: NixContext; value: Value; b: bool): nix_err {.nix_api_value.}
+
+proc init_string*(context: NixContext; value: Value; str: cstring): nix_err {.nix_api_value.}
+
+proc init_path_string*(context: NixContext; s: EvalState; value: Value; str: cstring): nix_err {.nix_api_value.}
+
+proc init_float*(context: NixContext; value: Value; d: cdouble): nix_err {.nix_api_value.}
+
+proc init_int*(context: NixContext; value: Value; i: int64): nix_err {.nix_api_value.}
+
+proc init_null*(context: NixContext; value: Value): nix_err {.nix_api_value.}
+
+proc init_apply*(context: NixContext; value: Value; fn: Value; arg: Value): nix_err {.nix_api_value.}
+
+proc init_external*(context: NixContext; value: Value; val: ExternalValue): nix_err {.nix_api_value.}
+
+proc make_list*(context: NixContext; list_builder: ListBuilder; value: Value): nix_err {.nix_api_value.}
+
+proc make_list_builder*(context: NixContext; state: EvalState; capacity: csize_t): ListBuilder {.nix_api_value.}
+
+proc list_builder_insert*(context: NixContext; list_builder: ListBuilder; index: cuint; value: Value): nix_err {.nix_api_value.}
+
+proc list_builder_free*(list_builder: ListBuilder) {.nix_api_value.}
+
+proc make_attrs*(context: NixContext; value: Value; b: BindingsBuilder): nix_err {.nix_api_value.}
+
+# proc init_primop*(context: NixContext; value: Value; op: PrimOp): nix_err {.nix_api_value.}
+
+proc copy_value*(context: NixContext; value: Value; source: Value): nix_err {.nix_api_value.}
+
+proc make_bindings_builder*(context: NixContext; state: EvalState; capacity: csize_t): BindingsBuilder {.nix_api_value.}
+
+proc bindings_builder_insert*(context: NixContext; builder: BindingsBuilder; name: cstring; value: Value): nix_err {.nix_api_value.}
+
+proc bindings_builder_free*(builder: BindingsBuilder) {.nix_api_value.}
+
+proc string_realise*(context: NixContext; state: EvalState; value: Value; isIFD: bool): RealisedString {.nix_api_value.}
+
+proc realised_string_get_buffer_start*(realised_string: RealisedString): cstring {.nix_api_value.}
+
+proc realised_string_get_buffer_size*(realised_string: RealisedString): csize_t {.nix_api_value.}
+
+proc realised_string_get_store_path_count*(realised_string: RealisedString): csize_t {.nix_api_value.}
+
+proc realised_string_get_store_path*(realised_string: RealisedString; index: csize_t): StorePath {.nix_api_value.}
+
+proc realised_string_free*(realised_string: RealisedString) {.nix_api_value.}
diff --git a/users/emery/nix-actor/src/nix_actor/nix_values.nim b/users/emery/nix-actor/src/nix_actor/nix_values.nim
new file mode 100644
index 000000000000..6eeadda66ae2
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/nix_values.nim
@@ -0,0 +1,235 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+import
+  std/options,
+  pkg/preserves,
+  pkg/syndicate/actors,
+  ./[nix_api, nix_api_util, nix_api_value, protocol, utils]
+
+type
+  Value = preserves.Value
+  NixValue* = nix_api.Value
+
+  StringThunkRef = ref StringThunkObj
+  StringThunkObj {.final.} = object of EmbeddedObj
+    data: Option[string]
+
+proc thunkString(start: cstring; n: cuint; state: pointer) {.cdecl.} =
+  let thunk = cast[ptr StringThunkObj](state)
+  assert thunk.data.isNone
+  var buf = newString(n)
+  if n > 0:
+    copyMem(buf[0].addr, start, buf.len)
+  thunk.data = buf.move.some
+
+proc unthunk(v: Value): Value =
+  let thunk = v.unembed(StringThunkRef)
+  result =
+    if thunk.isSome and thunk.get.data.isSome:
+      thunk.get.data.get.toPreserves
+    else: v
+
+proc unthunkAll*(v: Value): Value =
+  v.mapEmbeds(unthunk)
+
+proc callThru(nix: NixContext; state: EvalState; nv: NixValue): NixValue =
+  result = nv
+  while true:
+    case nix.get_type(result)
+    of NIX_TYPE_THUNK:
+      state.force(result)
+    of NIX_TYPE_FUNCTION:
+      # Call functions with empty attrsets.
+      var
+        args = nix.alloc_value(state)
+        bb = nix.make_bindings_builder(state, 0)
+      checkError nix.gc_decref(args)
+      doAssert nix.make_attrs(args, bb) == NIX_OK
+      bindings_builder_free(bb)
+      result = state.apply(result, args)
+    else:
+      return
+
+proc toPreserves*(value: NixValue; state: EvalState; nix: NixContext): Value {.gcsafe.} =
+  var value = nix.callThru(state, value)
+
+  let kind = nix.get_type(value)
+  case kind
+  of NIX_TYPE_INT:
+    result = nix.getInt(value).toPreserves
+  of NIX_TYPE_FLOAT:
+    result = nix.getFloat(value).toPreserves
+  of NIX_TYPE_BOOL:
+    result = nix.getBool(value).toPreserves
+  of NIX_TYPE_STRING:
+    let thunk = StringThunkRef()
+    let err = nix.getString(value, thunkString, thunk[].addr)
+    doAssert err == NIX_OK, $err
+    result = thunk.embed
+  of NIX_TYPE_PATH:
+    result = ($nix.getPathString(value)).toPreserves
+  of NIX_TYPE_NULL:
+    result = initRecord("null")
+  of NIX_TYPE_ATTRS:
+    if nix.has_attr_byname(value, state, "__toString"):
+      var str = nix.get_attr_byname(value, state, "__toString")
+      if nix.get_type(str) == NIX_TYPE_FUNCTION:
+        str = state.apply(str, value)
+      result = str.toPreserves(state, nix)
+    elif nix.has_attr_byname(value, state, "outPath"):
+      var outPath = nix.get_attr_byname(value, state, "outPath")
+      result = outPath.toPreserves(state, nix)
+    else:
+      let n = nix.getAttrsSize(value)
+      result = initDictionary(int n)
+      var i: cuint
+      while i < n:
+        let (key, val) = get_attr_byidx(value, state, i)
+        result[($key).toSymbol] = val.toPreserves(state, nix)
+        inc(i)
+  of NIX_TYPE_LIST:
+    let n = nix.getListSize(value)
+    result = initSequence(n)
+    var i: cuint
+    while i < n:
+      var val = nix.getListByIdx(value, state, i)
+      result[i] = val.toPreserves(state, nix)
+      inc(i)
+  of NIX_TYPE_THUNK, NIX_TYPE_FUNCTION:
+    raiseAssert "cannot preserve thunk or function"
+  of NIX_TYPE_EXTERNAL:
+    result = "«external»".toPreserves
+
+proc toPreserves*(value: NixValue; state: EvalState): Value {.gcsafe.} =
+  mitNix: result = toPreserves(value, state, nix)
+
+proc translate*(nix: NixContext; state: EvalState; pr: preserves.Value): NixValue =
+  try:
+    result = nix.alloc_value(state)
+    case pr.kind
+    of pkBoolean:
+      checkError nix.init_bool(result, pr.bool)
+    of pkFloat:
+      checkError nix.init_float(result, pr.float.cdouble)
+    of pkRegister:
+      checkError nix.init_int(result, pr.register.int64)
+    of pkBigInt:
+      checkError nix.init_int(result, pr.register.int64)
+    of pkString:
+      checkError nix.init_string(result, pr.string)
+    of pkByteString:
+      raise newException(ValueError, "cannot convert large Preserves integer to Nix: " & $pr)
+    of pkSymbol:
+      nix.evalFromString(state, cast[string](pr.symbol), "", result)
+    of pkRecord:
+      if pr.isRecord("null", 0):
+        checkError nix.init_null(result)
+      else:
+        raise newException(ValueError, "cannot convert Preserves record to Nix: " & $pr)
+    of pkSequence:
+      let b = nix.make_list_builder(state, pr.len.csize_t)
+      defer: list_builder_free(b)
+      for i, e in pr.sequence:
+        checkError nix.list_builder_insert(b, i.cuint, nix.translate(state, e))
+      checkError nix.make_list(b, result)
+    of pkSet:
+      raise newException(ValueError, "cannot convert Preserves sets to Nix")
+    of pkDictionary:
+      let b = nix.make_bindings_builder(state, pr.dict.len.csize_t)
+      defer: bindings_builder_free(b)
+      for (name, value) in pr.pairs:
+        if name.isSymbol:
+          checkError nix.bindings_builder_insert(b, name.symbol.string, nix.translate(state, value))
+        else:
+          checkError nix.bindings_builder_insert(b, $name, nix.translate(state, value))
+      checkError nix.make_attrs(result, b)
+    of pkEmbedded:
+      raise newException(ValueError, "cannot convert Preserves embedded value to Nix")
+  except CatchableError as err:
+    result.close()
+    raise err
+
+proc toNix*(pr: preserves.Value; state: EvalState): NixValue =
+  mitNix:
+    result = nix.translate(state, pr)
+
+proc step*(state: EvalState; nv: NixValue; path: openarray[preserves.Value]): Option[preserves.Value] =
+  mitNix:
+    var
+      nv = nix.callThru(state, nv)
+      i = 0
+    while i < path.len:
+      if nv.isNil: return
+      var kind = nix.get_type(nv)
+      case kind
+      of NIX_TYPE_ATTRS:
+        var key: string
+        case path[i].kind
+        of pkString:
+          key = path[i].string
+        of pkSymbol:
+          key = path[i].symbol.string
+        else:
+          key = $path[i]
+        if not nix.has_attr_byname(nv, state, key): return
+        var ctx: NixContext
+        nv = nix.get_attr_byname(nv, state, key)
+        inc i
+      of NIX_TYPE_LIST:
+        var ix: cuint
+        if not ix.fromPreserves(path[i]): return
+        nv = nix.get_list_byidx(nv, state, ix)
+        inc i
+      else:
+        raiseAssert("cannot step " & $kind)
+    result = nv.toPreserves(state, nix).some
+  assert path.len > 0 or result.isSome
+
+proc realiseString*(nix: NixContext; state: EvalState; val: NixValue): string =
+  var rs = nix.string_realise(state, val, false)
+  if rs.isNil: raise newException(nix)
+  result = newString(realised_string_get_buffer_size(rs))
+  if result.len > 0:
+    copyMem(result[0].addr, realised_string_get_buffer_start(rs), result.len)
+  realised_string_free(rs)
+
+proc realiseString*(state: EvalState; val: NixValue): string =
+  mitNix: result = nix.realiseString(state, val)
+
+proc initNull*(state: EvalState): NixValue =
+  mitNix:
+    result = nix.alloc_value(state)
+    checkError nix.init_null(result)
+
+proc typeName*(val: NixValue): string =
+  mitNix:
+    result = $nix.get_type(val)
+    # result = $nix.get_typename(val)
+
+proc isThunk*(value: NixValue): bool =
+  mitNix: result = nix.get_type(value) == NIX_TYPE_THUNK
+
+proc isLiteral*(value: NixValue): bool =
+  mitNix:
+    let kind = nix.get_type(value)
+    result =
+      case kind
+      of NIX_TYPE_INT,
+          NIX_TYPE_FLOAT,
+          NIX_TYPE_BOOL,
+          NIX_TYPE_STRING,
+          NIX_TYPE_PATH,
+          NIX_TYPE_NULL,
+          NIX_TYPE_ATTRS,
+          NIX_TYPE_LIST:
+        true
+      of NIX_TYPE_THUNK, NIX_TYPE_FUNCTION,
+          NIX_TYPE_EXTERNAL:
+        false
+
+proc isNull*(value: NixValue): bool =
+  mitNix: result = nix.get_type(value) == NIX_TYPE_NULL
+
+proc isFunc*(value: NixValue): bool =
+  mitNix: result = nix.get_type(value) == NIX_TYPE_FUNCTION
diff --git a/users/emery/nix-actor/src/nix_actor/protocol.nim b/users/emery/nix-actor/src/nix_actor/protocol.nim
new file mode 100644
index 000000000000..bf5935e26670
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/protocol.nim
@@ -0,0 +1,129 @@
+
+import
+  preserves, std/tables, std/options
+
+type
+  EvalResolveDetailLookupPath* = Option[seq[string]]
+  EvalResolveDetailStoreUri* = Option[string]
+  `EvalResolveDetail`* {.preservesDictionary.} = object
+    `lookupPath`*: Option[seq[string]]
+    `storeParams`*: Option[AttrSet]
+    `storeUri`*: Option[string]
+
+  Eval* {.preservesRecord: "eval".} = object
+    `expr`*: string
+    `args`*: Value
+    `result`* {.preservesEmbedded.}: EmbeddedRef
+
+  Error* {.preservesRecord: "error".} = object
+    `message`*: Value
+
+  AttrSet* = Table[Symbol, Value]
+  LookupPathKind* {.pure.} = enum
+    `lookupPath`, `absent`
+  LookupPathLookupPath* {.preservesDictionary.} = object
+    `lookupPath`*: seq[string]
+
+  LookupPathAbsent* {.preservesDictionary.} = object
+  
+  `LookupPath`* {.preservesOr.} = object
+    case orKind*: LookupPathKind
+    of LookupPathKind.`lookupPath`:
+        `lookuppath`*: LookupPathLookupPath
+
+    of LookupPathKind.`absent`:
+        `absent`*: LookupPathAbsent
+
+  
+  StoreResolveDetailStoreUri* = string
+  `StoreResolveDetail`* {.preservesDictionary.} = object
+    `storeParams`*: Option[AttrSet]
+    `storeUri`*: string
+
+  StoreParamsKind* {.pure.} = enum
+    `storeParams`, `absent`
+  StoreParamsStoreParams* {.preservesDictionary.} = object
+    `storeParams`*: AttrSet
+
+  StoreParamsAbsent* {.preservesDictionary.} = object
+  
+  `StoreParams`* {.preservesOr.} = object
+    case orKind*: StoreParamsKind
+    of StoreParamsKind.`storeParams`:
+        `storeparams`*: StoreParamsStoreParams
+
+    of StoreParamsKind.`absent`:
+        `absent`*: StoreParamsAbsent
+
+  
+  ResultKind* {.pure.} = enum
+    `Error`, `ok`
+  ResultOk* {.preservesRecord: "ok".} = object
+    `value`*: Value
+
+  `Result`* {.preservesOr.} = object
+    case orKind*: ResultKind
+    of ResultKind.`Error`:
+        `error`*: Error
+
+    of ResultKind.`ok`:
+        `ok`*: ResultOk
+
+  
+  RealiseString* {.preservesRecord: "realise-string".} = object
+    `result`* {.preservesEmbedded.}: EmbeddedRef
+
+  CheckStorePath* {.preservesRecord: "check-path".} = object
+    `path`*: string
+    `valid`* {.preservesEmbedded.}: EmbeddedRef
+
+  StoreUriKind* {.pure.} = enum
+    `storeUri`, `absent`
+  StoreUriStoreUri* {.preservesDictionary.} = object
+    `storeUri`*: string
+
+  StoreUriAbsent* {.preservesDictionary.} = object
+  
+  `StoreUri`* {.preservesOr.} = object
+    case orKind*: StoreUriKind
+    of StoreUriKind.`storeUri`:
+        `storeuri`*: StoreUriStoreUri
+
+    of StoreUriKind.`absent`:
+        `absent`*: StoreUriAbsent
+
+  
+  Replicate* {.preservesRecord: "replicate".} = object
+    `target`* {.preservesEmbedded.}: EmbeddedRef
+    `storePath`*: string
+    `result`* {.preservesEmbedded.}: EmbeddedRef
+
+  StoreResolveStep* {.preservesRecord: "nix-store".} = object
+    `detail`*: StoreResolveDetail
+
+  EvalResolveStep* {.preservesRecord: "nix".} = object
+    `detail`*: EvalResolveDetail
+
+proc `$`*(x: EvalResolveDetail | Eval | Error | AttrSet | LookupPath |
+    StoreResolveDetail |
+    StoreParams |
+    Result |
+    RealiseString |
+    CheckStorePath |
+    StoreUri |
+    Replicate |
+    StoreResolveStep |
+    EvalResolveStep): string =
+  `$`(toPreserves(x))
+
+proc encode*(x: EvalResolveDetail | Eval | Error | AttrSet | LookupPath |
+    StoreResolveDetail |
+    StoreParams |
+    Result |
+    RealiseString |
+    CheckStorePath |
+    StoreUri |
+    Replicate |
+    StoreResolveStep |
+    EvalResolveStep): seq[byte] =
+  encode(toPreserves(x))
diff --git a/users/emery/nix-actor/src/nix_actor/utils.nim b/users/emery/nix-actor/src/nix_actor/utils.nim
new file mode 100644
index 000000000000..ffe33b257b50
--- /dev/null
+++ b/users/emery/nix-actor/src/nix_actor/utils.nim
@@ -0,0 +1,26 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+import
+  ./nix_api_types,
+  ./nix_api_util
+
+proc newException*(ctx: NixContext): ref NixException =
+  new result
+  var
+    n: cuint
+    p = err_msg(NixContext(nil), ctx, addr n)
+  result.msg.setLen(n)
+  if n > 0:
+    copyMem(result.msg[0].addr, p, result.msg.len)
+
+template checkError*(code: nix_err) =
+  if code != NIX_OK: raise newException(nix)
+
+template mitNix*(body: untyped): untyped =
+  ## Mit nix machen.
+  block:
+    var nix {.inject.} = c_context_create()
+    defer: c_context_free(nix)
+    body
+    checkError err_code(nix)
diff --git a/users/emery/nix-actor/tests/Tupfile b/users/emery/nix-actor/tests/Tupfile
new file mode 100644
index 000000000000..09dc80a8dd72
--- /dev/null
+++ b/users/emery/nix-actor/tests/Tupfile
@@ -0,0 +1,2 @@
+include_rules
+: foreach t*.nim |> !balls |>
diff --git a/users/emery/nix-actor/tests/test.nim b/users/emery/nix-actor/tests/test.nim
new file mode 100644
index 000000000000..5e5fe23f5bac
--- /dev/null
+++ b/users/emery/nix-actor/tests/test.nim
@@ -0,0 +1,143 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+import
+  std/options,
+  pkg/balls,
+  pkg/sys/ioqueue,
+  pkg/preserves,
+  pkg/preserves/sugar,
+  pkg/syndicate,
+  ../src/nix_actor/[nix_api, nix_values, nix_api_value, nix_api_expr]
+
+type Value = preserves.Value
+
+initLibstore()
+initLibexpr()
+
+suite "libexpr":
+
+  let
+    store = openStore()
+    state = newState(store, ["nixpkgs"])
+
+  proc checkConversion(s: string) =
+    runActor("checkConversion") do (turn: Turn):
+      var nixVal = state.evalFromString(s, "")
+      state.force(nixVal)
+      nixVal.close()
+      var pr = nixVal.toPreserves(state)
+      checkpoint $pr
+      var wirePr = pr.unthunkAll
+      checkpoint $wirePr
+
+  test "lists":
+    let samples = [
+        "[]",
+        "[null]",
+        "[[]]",
+        "[ null [ null [null null null null null null null null ] null ] null ]",
+      ]
+    for s in samples:
+      test s:
+        checkConversion(s)
+
+  test "attrsets":
+    let samples = [
+        "{}",
+        "{a = {}; }",
+        "{a = { x = {}; }; b = null; c = null; d = null; e = null; }",
+      ]
+    for s in samples:
+      test s:
+        checkConversion(s)
+
+  test "derivation":
+    let samples = [
+        "let depot = import /depot { }; in depot.users.emery.pkgs.syndicate-server"
+      ]
+    for s in samples:
+      test s:
+        checkConversion(s)
+
+  test "large":
+    let samples =
+      "builtins.listToAttrs (builtins.genList (x: { name = toString x; value = null; }) 99)"
+    checkConversion(samples)
+
+type AddToStoreClientAttrs {.preservesDictionary.} = object
+  ## A subset of AddToStoreAttrs
+  `ca-method`: Symbol
+  eris: seq[byte]
+  name: string
+
+suite "fromPreserve":
+  const raw = "{ca: <bind <_>> ca-method: |fixed:r:sha256| deriver: <bind <_>> eris: #[CgA1VVrR0k5gjgU1wKQKVZr1RkANf4zUva3vyc2wmLzhzuL8XqeUL0HE4W3aRpXNwXyFbaLxtXJiLCUWSyLjej+h] name: \"default-builder.sh\" narHash: <bind <_>> narSize: <bind <_>> references: [] registrationTime: <bind <_>> sigs: <bind <_>> ultimate: <bind <_>>}"
+
+  let pr = parsePreserves(raw)
+  var attrs: AddToStoreClientAttrs
+  check fromPreserve(attrs, pr)
+
+suite "eval":
+  let
+    store = openStore()
+    eval = store.newState()
+
+  test "function":
+    let
+      fn = eval.evalFromString("x: y: x + y")
+      x = (%"foo").toNix(eval)
+      y = (%"bar").toNix(eval)
+    checkpoint "fn:", fn.typeName
+    checkpoint " x:", x.typeName
+    checkpoint " y:", y.typeName
+    let
+      r = eval.apply(eval.apply(fn, x), y)
+      pr = r.toPreserves(eval).unthunkAll
+
+    checkpoint $pr
+    check $pr == """"foobar""""
+
+  eval.close()
+  store.close()
+
+suite "import":
+  let
+    store = openStore()
+    eval = store.newState()
+
+  block:
+    ## full expression string
+    let f = eval.evalFromString("false")
+    let t = eval.evalFromString("true")
+    let fs = $f.toPreserves(eval)
+    let ts = $t.toPreserves(eval)
+    checkpoint "false:", fs
+    check fs == "#f", fs
+    check ts == "#t", ts
+
+  block:
+    ## full expression string
+    let fn = eval.evalFromString("x: x")
+    let x = eval.evalFromString("let pkgs = import /home/repo/nixpkgs {}; in pkgs.ncdc.meta.homepage")
+    eval.force(x)
+    let y = eval.apply(fn, x)
+    let pr = $y.toPreserves(eval).unthunkAll
+    checkpoint "$y.toPreserves(eval):", pr
+    check pr == """"https://dev.yorhel.nl/ncdc""""
+
+  block:
+    ## function
+    let pre = eval.evalFromString("import /home/repo/nixpkgs")
+    let args = eval.evalFromString("{ }")
+    let pkgs = eval.apply(pre, args)
+    let fn = eval.evalFromString("""pkgs: pkgs.ncdc.meta.homepage""")
+    let res = eval.apply(fn, pkgs)
+    let pr = res.toPreserves(eval).unthunkAll
+    assert not pr.isEmbedded
+    let text = $pr
+    checkpoint text
+    check text == """"https://dev.yorhel.nl/ncdc""""
+
+  eval.close()
+  store.close()
diff --git a/users/emery/nix-actor/tests/test_actor.nim b/users/emery/nix-actor/tests/test_actor.nim
new file mode 100644
index 000000000000..2a7aab92a2fa
--- /dev/null
+++ b/users/emery/nix-actor/tests/test_actor.nim
@@ -0,0 +1,191 @@
+# SPDX-FileCopyrightText: ☭ Emery Hemingway
+# SPDX-License-Identifier: Unlicense
+
+import
+  std/options,
+  pkg/balls,
+  pkg/sys/ioqueue,
+  pkg/preserves,
+  pkg/preserves/sugar,
+  pkg/syndicate,
+  pkg/syndicate/protocols/[gatekeeper, rpc],
+  ../src/nix_actor,
+  ../src/nix_actor/[nix_api, nix_values, protocol]
+
+type Value = preserves.Value
+
+initLibstore()
+initLibexpr()
+
+type
+  ResultContinuation {.final.} = ref object of Entity
+    cb: proc (turn: Turn; v: Value)
+
+method publish(cont: ResultContinuation; turn: Turn; ass: AssertionRef; h: Handle) =
+  cont.cb(turn, ass.value)
+
+proc newResultContinuation[T](turn: Turn; cb: proc (turn: Turn; v: T)): Cap =
+  proc wrapper(turn: Turn; v: Value) =
+    var
+      err: ResultError
+      ok: ResultOk
+    if err.fromPreserves(v):
+      raiseAssert $err.error
+    check ok.fromPreserves(v)
+    var x = ok.value.preservesTo(T)
+    check x.isSome
+    if x.isSome: cb(turn, x.get)
+  turn.newCap(ResultContinuation(cb: wrapper))
+
+suite "basic":
+
+  var completed: bool
+
+  proc actorTest(turn: Turn) =
+    turn.onStop do (turn: Turn):
+      block:
+        ## actor stopped
+        check completed
+
+    checkpoint "actor booted"
+    let rootFacet = turn.facet
+    let ds = turn.newDataspace()
+
+    let stepC = newResultContinuation(turn) do (turn: Turn; nix: Cap):
+      checkpoint "stepC"
+      block:
+        ## stepC
+        onPublish(turn, nix, grab()) do (v: Value):
+          checkpoint("stepC grabbed nix value " & $v)
+          assert not v.isRecord("null")
+          assert v == %"Hello VolgaSprint!"
+          completed = true
+          stop(rootFacet)
+
+    let stepB = newResultContinuation(turn) do (turn: Turn; nix: Cap):
+      checkpoint "stepB"
+      block:
+        ## stepB
+        onPublish(turn, nix, grab()) do (v: Value):
+          checkpoint("stepB grabbed nix value " & $v)
+          assert not v.isRecord("null")
+          check v == %"Hello Volga"
+        publish(turn, nix, Eval(
+            expr: "x: y: x + y",
+            args: %"Sprint!",
+            result: stepC
+          ))
+
+    let stepA = newResultContinuation(turn) do (turn: Turn; nix: Cap):
+      checkpoint "stepA"
+      block:
+        ## stepA
+        onPublish(turn, nix, grab()) do (v: Value):
+          checkpoint "stepA grabbed nix value " & $v
+          assert not v.isRecord("null")
+          check v == %"Hello"
+        publish(turn, nix, Eval(
+            expr: "x: y: x + y",
+            args: %" Volga",
+            result: stepB
+          ))
+
+    during(turn, ds, ResolvedAccepted.grabWithin) do (nix: Cap):
+      checkpoint "resolve accepted"
+      block:
+        ## Resolved nix actor through gatekeeper
+        onPublish(turn, nix, grab()) do (v: Value):
+          checkpoint $v
+        publish(turn, nix, Eval(
+            expr: "x: y: y",
+            args: %"Hello",
+            result: stepA,
+          ))
+
+    during(turn, ds, Rejected.grabType) do (rej: Rejected):
+      raiseAssert("resolve failed: " & $rej)
+
+    publish(turn, ds, Resolve(
+        step: parsePreserves"""<nix { }>""",
+        observer: ds,
+      ))
+
+    nix_actor.bootActor(turn, ds)
+
+  block:
+    ## runActor
+    runActor("tests", actorTest)
+    check completed
+
+suite "nixpkgs":
+
+  var completed: bool
+
+  proc actorTest(turn: Turn) =
+    turn.onStop do (turn: Turn):
+      block:
+        ## actor stopped
+        check completed
+
+    checkpoint "actor booted"
+    let rootFacet = turn.facet
+    let ds = turn.newDataspace()
+
+    let stepC = newResultContinuation(turn) do (turn: Turn; nix: Cap):
+      checkpoint "stepC"
+      block:
+        ## stepC
+        onPublish(turn, nix, grab()) do (v: Value):
+          checkpoint("stepC grabbed nix value " & $v)
+          assert v == %"https://9fans.github.io/plan9port/"
+          completed = true
+          stop(rootFacet)
+
+    let stepB = newResultContinuation(turn) do (turn: Turn; nix: Cap):
+      checkpoint "stepB"
+      block:
+        ## stepB
+        publish(turn, nix, Eval(
+            expr: "pkg: _: pkg.meta.homepage",
+            args: %false,
+            result: stepC
+          ))
+
+    let stepA = newResultContinuation(turn) do (turn: Turn; nix: Cap):
+      checkpoint "stepA"
+      block:
+        ## stepA
+        publish(turn, nix, Eval(
+            expr: "pkgs: name: builtins.getAttr name pkgs",
+            args: %"plan9port",
+            result: stepB
+          ))
+
+    during(turn, ds, ResolvedAccepted.grabWithin) do (nix: Cap):
+      checkpoint "resolve accepted"
+      block:
+        ## Resolved nix actor through gatekeeper
+        onPublish(turn, nix, grab()) do (v: Value):
+          checkpoint $v
+        publish(turn, nix, Eval(
+            expr: "_: args: import <nixpkgs> args",
+            args: initDictionary(),
+            result: stepA,
+          ))
+
+    during(turn, ds, Rejected.grabType) do (rej: Rejected):
+      raiseAssert("resolve failed: " & $rej)
+
+    publish(turn, ds, Resolve(
+        step: parsePreserves"""
+            <nix { lookupPath: [ "nixpkgs=/home/repo/nixpkgs/channel" ] }>
+          """,
+        observer: ds,
+      ))
+
+    nix_actor.bootActor(turn, ds)
+
+  block:
+    ## runActor
+    runActor("tests", actorTest)
+    check completed
diff --git a/users/emery/nix-actor/tup.config.nix b/users/emery/nix-actor/tup.config.nix
new file mode 100644
index 000000000000..892248b60eba
--- /dev/null
+++ b/users/emery/nix-actor/tup.config.nix
@@ -0,0 +1,16 @@
+{ lib, pkgs, ... }:
+
+let
+  nix' = pkgs.nixVersions.latest.overrideAttrs (_: {
+    version = "2024-08-23";
+    src = pkgs.fetchFromGitHub {
+      owner = "nixos";
+      repo = "nix";
+      rev = "85f1aa6b3df5c5fcc924a74e2a9cc8acea9ba0e1";
+      hash = "sha256-3+UgAktTtkGUNpxMxr+q+R+z3r026L3PwJzG6RD2IXM=";
+    };
+  });
+in
+{
+  PKG_CONFIG_PATH_nix = "${lib.getDev nix'}/lib/pkgconfig";
+}