Gate your pipeline
Make a policy regression or a posture finding fail the build, with no interactive prompts.
Two commands are designed for CI. Both exit non-zero on failure and neither needs a terminal.
| Command | Fails when |
|---|---|
veefore test |
A policy test case fails, or a dangerous tool is permitted only by a default decision |
veefore doctor --strict |
Any posture finding, including warnings |
#Non-interactive authentication
Prompts fall back to defaults when stdin is not a TTY, and the secret prompt refuses outright rather than hanging. Supply credentials as environment variables:
bashVEEFORE_EMAIL=ci@example.com
VEEFORE_PASSWORD=…
VEEFORE_MFA_CODE=… # only where MFA is enforced
VEEFORE_CONTROL_PLANE_URL=https://veefore.your-company.com # self-hosted only
On hosted Veefore, leave VEEFORE_CONTROL_PLANE_URL unset. A self-hosted
address is usually not needed either, because the CLI reads it from the
project's committed veefore.config.json; set it when the job runs outside a
set-up project.
An API key is better than a password where the commands support it, because it is scoped to a role and revocable on its own. See API keys.
Never use an `OWNER` key or password in CI
A CI secret lives in your CI provider's storage, is readable by every build step, and ends up in logs if anyone echoes the environment.
Create a custom role with exactly what the gate needs — policy:read,
policy:simulate, agent:read, tool:read — and nothing else.
#GitHub Actions
yamlname: Veefore policy gate
on:
pull_request:
push:
branches: [main]
jobs:
policy:
runs-on: ubuntu-latest
env:
VEEFORE_CONTROL_PLANE_URL: ${{ vars.VEEFORE_CONTROL_PLANE_URL }} # self-hosted only
VEEFORE_EMAIL: ${{ secrets.VEEFORE_EMAIL }}
VEEFORE_PASSWORD: ${{ secrets.VEEFORE_PASSWORD }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm install -g veefore
# Policy test cases must pass, and no dangerous tool may be reachable
# only because no rule covers it.
- run: veefore test
# Posture audit. --strict makes warnings fail too.
- run: veefore doctor --strict
#GitLab CI
yamlveefore-policy-gate:
image: node:20
variables:
VEEFORE_CONTROL_PLANE_URL: $VEEFORE_CONTROL_PLANE_URL # self-hosted only
script:
- npm ci
- npm install -g veefore
- veefore test
- veefore doctor --strict
Set VEEFORE_EMAIL and VEEFORE_PASSWORD as masked, protected CI variables.
#What veefore test actually checks
- A dry-run probe across every tool this agent is authorized for. Catches an agent that is authorized for a tool nobody intended, and a tool whose schema has drifted.
- Every policy test case. The Given/Then cases recorded on the policy versions in scope.
- Dangerous tool coverage. Fails if a tool marked
dangerous: trueis permitted only by adefaultDecisionwith no rule covering it. This is the check most likely to catch a real problem — a destructive tool that is allowed because nobody wrote a rule, not because anyone decided it should be.
bashveefore test --tool refund_customer # probe one tool
veefore test --json # machine-readable
#Gate on activation, not just on tests
Activation already requires passing tests, step-up authentication and a reason, and step-up permissions are unavailable to API keys — so CI cannot activate a policy on its own. That is deliberate: activation moves the authorization boundary for every agent in scope, and it is a decision rather than a build step.
The workable shape is:
yaml # CI proves the policy is correct and nothing regressed.
- run: veefore test
- run: veefore doctor --strict
# A human activates, with step-up, from their own session.
# veefore policies deploy pol_01ABC --reason "CHG-4411"
#Fail the build on a bad decision shape
Simulation is side-effect free, so you can assert on specific request shapes from a test:
tsimport { describe, expect, it } from 'vitest';
const BASE = process.env.VEEFORE_CONTROL_PLANE_URL ?? 'https://veefore.example.com';
const KEY = process.env.VEEFORE_API_KEY;
async function simulate(body: Record<string, unknown>) {
const response = await fetch(`${BASE}/v1/policies/simulate`, {
method: 'POST',
headers: { authorization: `Bearer ${KEY}`, 'content-type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`simulate failed: ${response.status}`);
return (await response.json()).data;
}
describe('refund policy', () => {
it('allows small refunds', async () => {
const result = await simulate({ agentId: AGENT, tool: 'refund_customer', amount: 50_00 });
expect(result.decision).toBe('ALLOW');
});
it('requires approval above the ceiling', async () => {
const result = await simulate({ agentId: AGENT, tool: 'refund_customer', amount: 1_500_00 });
expect(result.decision).toBe('REQUIRE_APPROVAL');
});
it('refuses destructive database operations', async () => {
const result = await simulate({
agentId: AGENT,
tool: 'run_query',
parameters: { operation: 'DELETE' },
});
expect(result.decision).toBe('BLOCK');
});
});
Prefer recorded policy test cases where you can — they also run at activation time, which is the moment that matters. Use this pattern for assertions that span several policies, or that depend on live state the recorded cases cannot express.
#Verifying Veefore itself
If you run Veefore from source, the repository has its own gate:
bashnpm run verify
Twenty-three stages: typecheck, lint, format, the scope audit, the quality audit, the migration check, seven test suites, five smoke tests against real processes, infrastructure validation, the console build and the docs check.
Optional stages are skipped with a named reason rather than silently omitted, so a green run cannot hide a missing dependency.
bashnpm run verify -- --bail # stop at the first failure
npm run verify -- --only=policy # one stage
VERIFY_QUIET=1 npm run verify # capture output, print only on failure