Skip to content

Independent TypeScript Consumer

Use the published @axisvela/praxo-core@0.7.1 package in your own Node.js application. Node.js 20 or newer is required. No PRAXO-CORE source checkout, API key, model service, or AxisVela product is needed. Installation needs npm registry access or a populated cache.

Choose the Boundary

This fits an embedded model/tool loop with explicit tool scope and policy. Your application still owns model access, authentic identity, persistent approvals, external effects, and isolation. It is not a hosted agent platform, durable graph engine, or sandbox service. See when not to use Core.

Create the Application

In a new application directory, create package.json with an exact dependency, then install from the lockfile:

npm install --save-exact @axisvela/praxo-core@0.7.1

Commit the generated package-lock.json, and use npm ci in repeatable checks. Do not use file:, link:, a repository URL, or a local path to substitute the package. The tests verify the pinned version and that the module resolves inside the application.

Application Source

Use the following as starter.js, rendered directly from the tested source.

const {
  Agent,
  FuncHook,
  Registry,
  ROLE_ASSISTANT,
  ROLE_TOOL,
  PolicyDeniedError,
} = require("@axisvela/praxo-core");

class FixtureModel {
  constructor(target) {
    this.target = target;
  }

  async generate(request) {
    const last = request.messages[request.messages.length - 1];
    if (last.role === ROLE_TOOL) {
      return { message: { role: ROLE_ASSISTANT, content: "recorded" } };
    }
    return {
      message: {
        role: ROLE_ASSISTANT,
        toolCalls: [
          {
            id: "record-1",
            name: "record",
            arguments: { target: this.target },
          },
        ],
      },
    };
  }
}

class RecordTool {
  constructor() {
    this.name = "record";
    this.description = "Record a target in an in-memory counter.";
    this.risk = "write";
    this.parameters = {
      type: "object",
      required: ["target"],
      properties: { target: { type: "string" } },
    };
    this.calls = 0;
  }

  async invoke() {
    this.calls += 1;
    return "recorded";
  }
}

function buildAgent(target) {
  const tool = new RecordTool();
  const tools = new Registry();
  tools.register(tool);
  const policy = new FuncHook((action) => {
    if (action.kind === "model.generate") {
      return { allowed: true };
    }
    if (action.kind === "tool.invoke") {
      return {
        allowed:
          action.toolName === "record" &&
          action.arguments?.target === "public",
        reason: "only the public target is permitted",
      };
    }
    return { allowed: false, reason: "unsupported action" };
  });
  const agent = new Agent({
    model: new FixtureModel(target),
    tools,
    maxSteps: 3,
    allowedTools: ["record"],
    policy,
  });
  return { agent, tool };
}

async function run(write = (line) => console.log(line)) {
  const allowed = buildAgent("public");
  const result = await allowed.agent.run(
    { id: "starter-public" },
    "Record the requested target."
  );
  if (result.output !== "recorded" || allowed.tool.calls !== 1) {
    throw new Error(
      `allow verification failed: output=${JSON.stringify(result.output)} effects=${allowed.tool.calls}`
    );
  }
  write("allow: recorded; effects=1");

  const denied = buildAgent("private");
  try {
    await denied.agent.run(
      { id: "starter-private" },
      "Record the requested target."
    );
  } catch (error) {
    if (!(error instanceof PolicyDeniedError)) {
      throw error;
    }
    if (denied.tool.calls !== 0) {
      throw new Error(`denied call produced effects=${denied.tool.calls}`);
    }
    write("deny: policy rejected; effects=0");
    return;
  }
  throw new Error("private target was not denied");
}

if (require.main === module) {
  run().catch((error) => {
    console.error(error);
    process.exitCode = 1;
  });
}

module.exports = { buildAgent, run };

Acceptance Tests

Use the following as test_starter.test.js, rendered from the tested source.

const assert = require("node:assert/strict");
const path = require("node:path");
const test = require("node:test");

const praxo = require("@axisvela/praxo-core");
const pkg = require("@axisvela/praxo-core/package.json");

const { buildAgent, run } = require("./starter");

const expectedVersion = process.env.PRAXO_EXPECTED_VERSION || "0.7.1";

test("allowed tool executes once", async () => {
  const { agent, tool } = buildAgent("public");
  const result = await agent.run(
    { id: "test-public" },
    "Record the requested target."
  );
  assert.equal(result.output, "recorded");
  assert.equal(tool.calls, 1);
});

test("denied tool has no effect", async () => {
  const { agent, tool } = buildAgent("private");
  await assert.rejects(
    agent.run({ id: "test-private" }, "Record the requested target."),
    praxo.PolicyDeniedError
  );
  assert.equal(tool.calls, 0);
});

test("starter output is deterministic", async () => {
  const lines = [];
  await run((line) => lines.push(line));
  assert.deepEqual(lines, [
    "allow: recorded; effects=1",
    "deny: policy rejected; effects=0",
  ]);
});

test("dependency comes from the published package", () => {
  assert.equal(pkg.version, expectedVersion);
  assert.equal(praxo.SDK_VERSION, pkg.version);
  const resolved = require.resolve("@axisvela/praxo-core");
  const root = path.resolve(__dirname);
  assert.ok(
    resolved.startsWith(root + path.sep),
    `dependency resolved outside the application: ${resolved}`
  );
  const dependency = require("./package.json").dependencies["@axisvela/praxo-core"];
  assert.equal(dependency, expectedVersion);
  assert.ok(!/^(file:|link:|git|https?:)/.test(dependency));
});

Verify

npm ci --no-audit --no-fund
npm test
node starter.js

Expected program output:

allow: recorded; effects=1
deny: policy rejected; effects=0

The model is a deterministic fixture, not an LLM. allowedTools limits the tool name; Policy also checks the target argument. A denied call throws PolicyDeniedError and must not increment the in-memory counter. An empty allowlist is not deny-all in the current API. Keep these tests when replacing the fixture with a real model adapter.

Governed Acceptance

governed.test.js adds five negative checks:

node --test governed.test.js
Check Required outcome
Missing mandatory policy application guard rejects before a run; zero tool effects
Empty tool scope application guard rejects; zero tool effects
Middleware changes final arguments final operation is reauthorized; denial prevents the effect
Approval edit changes arguments edited operation is reauthorized; denial prevents the effect
Policy service error run fails closed; zero tool effects

Use the tested source. The guard is application-level: Core keeps its ordinary nil-policy behavior. These checks do not prove authentic identity, durable approvals, effect reconciliation, or a general security profile.

These checks establish only the stated package and fixture behavior, not live model quality, authentic identity, isolation, durable effects, or production safety. Continue with the TypeScript implementation guide and the security model.