Freestyle Docs

Freestyle / Guides

How to Use Jev in a Sandbox

Call TypeSafe AI's Jev from a Freestyle VM with its API key held at the edge, then use typed decisions to route agent work.

Jev is TypeSafe AI’s decision model. It takes state plus named Noul, Choice, and Score questions, then returns structured answers for your code to use. It does not generate prose.

This guide runs TypeSafe’s JavaScript SDK inside a Freestyle VM while the real TypeSafe API key stays in your trusted controller. The VM supplies a placeholder key. A Freestyle HTTP egress rule replaces it only on POST /v1/systemone requests to api.typesafe.ai.

Prepare A Snapshot With The TypeSafe SDK

Install the Freestyle SDK on your controller and export its API key:

npm install freestyle@latest
export FREESTYLE_API_KEY="your-freestyle-api-key"
export TYPESAFE_API_KEY="your-typesafe-api-key"

Create the TypeSafe key in the TypeSafe console. Keep both keys in the controller’s secret store, outside the VM and its snapshots.

Start with the reusable Node.js snapshot from the Node.js sandbox guide, then install the official TypeSafe SDK into a dedicated application directory. Set the Node snapshot ID before running this builder:

export FREESTYLE_NODE_SNAPSHOT_ID="snapshot-id-from-the-node-guide"
import { Freestyle } from "freestyle";

function requiredEnv(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const freestyle = new Freestyle();
const { vm: builder } = await freestyle.vms.create({
  snapshotId: requiredEnv("FREESTYLE_NODE_SNAPSHOT_ID"),
  firewall: {
    rules: [{ action: "allow", source: {}, destination: { public: true } }],
  },
  slug: "jev-builder",
});

const node = "export HOME=/root NVM_DIR=/opt/nvm && . $NVM_DIR/nvm.sh &&";
try {
  const install = await builder.exec({
    command:
      `${node} mkdir -p /opt/jev-agent && cd /opt/jev-agent && ` +
      "npm init -y && npm pkg set type=module && npm install @typesafe-ai/sdk",
    timeoutMs: 300_000,
  });
  if (install.statusCode !== 0) {
    throw new Error(install.stderr ?? "TypeSafe SDK installation failed");
  }

  const { snapshotId } = await builder.snapshot();
  console.log({ snapshotId });
} finally {
  await builder.delete();
}

Run the builder and save the printed ID as FREESTYLE_JEV_SNAPSHOT_ID. The snapshot contains Node.js and the installed package, but neither API key.

Give One VM Access To Jev

Create a VM with no general Internet firewall rule. Then add one named TLS route that injects the TypeSafe Bearer key only on the System One endpoint:

import { Freestyle, type CreateTlsRuleOptions } from "freestyle";

function requiredEnv(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const freestyle = new Freestyle();
const { vm, vmId } = await freestyle.vms.create({
  snapshotId: requiredEnv("FREESTYLE_JEV_SNAPSHOT_ID"),
  firewall: { rules: [] },
  slug: "jev-agent",
});

function jevRule(apiKey: string): CreateTlsRuleOptions {
  return {
    action: "allow",
    domain: "api.typesafe.ai",
    source: { vmId },
    destination: { public: true },
    match: { method: ["POST"], path: { exact: "/v1/systemone" } },
    transform: [
      { headers: { authorization: `Bearer ${apiKey}` } },
    ],
  };
}

const rule = await freestyle.tls.rules.create(
  jevRule(requiredEnv("TYPESAFE_API_KEY")),
);
console.log({ vmId, ruleId: rule.id });

The rule steers the exact hostname through the Freestyle edge, installs the Freestyle CA in the VM’s system trust store, and opens the named route without granting raw-IP Internet access. Header values are sealed at rest and read back as "***".

Every process in this VM can use the grant. The secret is not readable from the guest, but the ability to spend it is available to code running there. Use a separate VM or VPC source when workloads need separate TypeSafe access.

Ask Several Typed Questions In One Request

Continue in jev-controller.ts after the rule creation. This writes an agent-side script into the prepared application directory:

await vm.fs.writeTextFile(
  "/opt/jev-agent/route-ticket.mjs",
  `import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient();
const response = await client.systemOne({
  state: {
    subject: "Checkout failed twice",
    body: "Our checkout has rejected every payment since this morning. Please help.",
  },
  questions: {
    isUrgent: noul(
      "Does this ticket describe an active problem that needs attention today?",
      {
        true: "The problem is active and time-sensitive",
        false: "The problem is informational or can wait",
      },
    ),
    owner: choice("Which team should own this ticket?", {
      billing: "Charges, invoices, refunds, or subscription state",
      technical: "Product failures, bugs, outages, or integrations",
      sales: "Pricing, plans, trials, or purchasing",
    }),
    frustration: score("How frustrated does the customer sound?", [
      "Calm",
      "Concerned",
      "Frustrated",
      "Angry",
    ]),
  },
});

console.log(JSON.stringify({
  model: response.model,
  owner: response.answers.owner.choice,
  ownerConfidence: response.answers.owner.confidence,
  urgencyProbability: response.answers.isUrgent.noul,
  frustrationScore: response.answers.frustration.score,
  frustrationConfidence: response.answers.frustration.confidence,
}, null, 2));
`,
);

Run it inside the VM. The SDK requires a non-empty API key locally, so pass a placeholder. Freestyle overwrites its Authorization header before the request reaches TypeSafe:

const node = "export HOME=/root NVM_DIR=/opt/nvm && . $NVM_DIR/nvm.sh &&";
let output = "";
let lastError = "Jev request failed";
for (let attempt = 0; attempt < 10; attempt++) {
  const result = await vm.exec({
    command: `${node} node /opt/jev-agent/route-ticket.mjs`,
    env: {
      TYPESAFE_API_KEY: "unused-placeholder",
      NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt",
    },
    timeoutMs: 60_000,
  });
  if (result.statusCode === 0) {
    output = result.stdout ?? "";
    break;
  }
  lastError = result.stderr ?? lastError;
  await new Promise((resolve) => setTimeout(resolve, 2_000));
}
if (!output) throw new Error(lastError);
console.log(output);

The SDK keeps its default base URL, https://api.typesafe.ai, and defaults to the jev-latest model alias. NODE_EXTRA_CA_CERTS makes Node trust the CA that Freestyle installed for the named egress route. Keep certificate verification enabled. The short retry loop allows the new route, hostname mapping, and CA installation to propagate.

The output contains a selected owner, a yes-probability for urgency, a probability-weighted frustration score, and confidence for the Choice and Score answers. Define action thresholds from an evaluation set that matches your own traffic. Do not copy a threshold from an unrelated example.

Put Jev In An Agent Loop

Use Jev for bounded decisions such as choosing a handler, ranking a shortlist, or deciding when a person should review a task. Keep arithmetic, authorization, and irreversible actions in ordinary code. Jev is not a replacement for the generative model that writes code or prose.

Treat user-controlled state as untrusted. TypeSafe’s Jev limitations note that adversarial wording in state can move an answer. A Jev result can raise a review flag, but it should not by itself authorize a payment, delete data, or bypass a human approval step.

The exact-path matcher selects when Freestyle injects the credential. It is not a deny rule. A different request to api.typesafe.ai, such as GET /v1/models, still follows the named route but keeps the placeholder credential and should fail authentication. Keep the VM’s firewall empty unless the workload needs another destination, then add each required route explicitly.

Rotate The Key Or Remove Access

After creating a replacement key in TypeSafe, load it into the controller and replace the complete rule:

await freestyle.tls.rules.update(
  rule.id,
  jevRule(requiredEnv("TYPESAFE_API_KEY")),
);

The update can take about five seconds to reach every edge cache. Verify a new request before revoking the old key. A rule read cannot reconstruct its redacted secret, so every update must include the real replacement value.

Delete the rule and VM when the worker no longer needs them:

await freestyle.tls.rules.delete(rule.id);
await vm.delete();

Deleting the Freestyle rule removes the VM’s grant. Revoke the provider key in TypeSafe when the credential itself should stop working elsewhere.

esc