Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ jobs:
run: |
make reviewable

conformance-drift:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

# The vendored agentconfig conformance file must match the API's main (a notice, not a
# failure, while the API has not published it yet). Not in `make reviewable`: it needs
# the network.
- name: Compare the agentconfig conformance file with api@main
run: scripts/sync-agentconfig-conformance.sh --check main

type-check:
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Vendored byte-for-byte from compliance-framework/api (see the README next to it).
src/utils/agent-config/__tests__/fixtures/agentconfig-conformance.json
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"format:fix": "prettier --write src/",
"format:staged": "lint-staged",
"format:check": "prettier --check src/",
"sync:agentconfig-conformance": "scripts/sync-agentconfig-conformance.sh",
"prepare": "husky"
},
"lint-staged": {
Expand Down
50 changes: 50 additions & 0 deletions scripts/sync-agentconfig-conformance.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Vendors the API's agentconfig conformance file (pkg/agentconfig/testdata/conformance.json)
# as the UI's fixture, byte for byte. See src/utils/agent-config/__tests__/fixtures/README.md.
#
# scripts/sync-agentconfig-conformance.sh [ref] copy api@<ref> (default main) over the fixture
# scripts/sync-agentconfig-conformance.sh --check [ref] fail when the fixture differs from api@<ref>;
# passes with a notice while <ref> has no file (404)
set -euo pipefail

check=false
if [ "${1:-}" = "--check" ]; then
check=true
shift
fi
ref="${1:-main}"
url="https://raw.githubusercontent.com/compliance-framework/api/${ref}/pkg/agentconfig/testdata/conformance.json"
fixture="src/utils/agent-config/__tests__/fixtures/agentconfig-conformance.json"
cd "$(dirname "$0")/.."

tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
status="$(curl -sSL -o "$tmp" -w '%{http_code}' "$url")"

if [ "$status" = "404" ]; then
if $check; then
echo "::notice title=agentconfig conformance::api@${ref} has no pkg/agentconfig/testdata/conformance.json yet; nothing to compare."
exit 0
fi
echo "error: api@${ref} has no pkg/agentconfig/testdata/conformance.json ($url)" >&2
exit 1
fi
if [ "$status" != "200" ]; then
echo "error: HTTP $status for $url" >&2
exit 1
fi

if $check; then
if cmp -s "$tmp" "$fixture"; then
echo "$fixture matches api@${ref}."
exit 0
fi
echo "::error file=${fixture}::The vendored conformance file differs from api@${ref}. Run scripts/sync-agentconfig-conformance.sh ${ref}, then fix the conformance cases that fail."
diff -u "$fixture" "$tmp" || true
exit 1
fi

cp "$tmp" "$fixture"
echo "Copied api@${ref} to $fixture."
echo "Record the API commit in src/utils/agent-config/__tests__/fixtures/README.md, then run:"
echo " npx vitest run src/utils/agent-config/__tests__/agentconfig-conformance.spec.ts"
4 changes: 4 additions & 0 deletions src/config/tooltips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export const TOOLTIPS = {
'statement.props': '', // TODO: Add tooltip
'statement.links': '', // TODO: Add tooltip

// Agent configuration
'agents.config.field.forbidden':
'Set on the agent host; can never be changed remotely',

// Add more tooltips here as needed
// 'feature.name': 'Tooltip text here',
} as const;
Expand Down
133 changes: 133 additions & 0 deletions src/utils/agent-config/__tests__/agentconfig-conformance.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// The UI's copies of the API's pkg/agentconfig rules against the API's golden file of expected
// results (fixtures/agentconfig-conformance.json, vendored byte for byte; see fixtures/README.md),
// so a rule that changes on one side and not the other fails here. Every table is run; the
// expectations all come from the file.
import { describe, expect, it } from 'vitest';
import type {
AgentInstanceSummary,
ConfigDoc,
OverlayDoc,
} from '@/types/agent-config';
import fixture from './fixtures/agentconfig-conformance.json';
import { configKeyOverridable, sourceTrusted } from '../glob';
import { validateCron5 } from '../cron5';
import { fieldAccess, sourceKind } from '../field-access';

interface Conformance {
trustedSources: {
patterns: string[];
cases: [string, boolean][];
extra: { patterns: string[]; source: string; want: boolean }[];
};
overridableConfigFlags: {
name: string;
flags: string[];
plugin: string;
key: string;
want: boolean;
}[];
sourceKinds: [string, 'oci' | 'local'][];
schedules: { valid: string[]; invalid: string[] };
applySafe: {
cases: {
name: string;
trusted: string[];
file: NonNullable<ConfigDoc['plugins']>;
overlay: OverlayDoc | null;
path: string;
state: string;
}[];
};
}

const cases = fixture as unknown as Conformance;

function applySafe(trusted: string[]): AgentInstanceSummary {
return {
instanceId: 'h',
hostname: 'h',
agentVersion: null,
mode: 'apply_safe',
firstSeenAt: '',
lastSeenAt: '',
reportedAt: '2026-10-01T00:00:00Z',
stale: false,
appliedRevision: 1,
attemptedRevision: 1,
status: 'applied',
reason: null,
error: null,
syncStatus: 'in-sync',
effectiveDigest: null,
heartbeatConfigRevision: 1,
reportStale: false,
unsafe: [],
remoteConfig: { mode: 'apply_safe', trusted_sources: trusted },
};
}

describe('pkg/agentconfig conformance', () => {
it('knows every table of the golden file', () => {
// A table the API adds must get a runner here (pluginNames runs with validation.ts).
expect(
Object.keys(fixture)
.filter((k) => !k.startsWith('_'))
.sort(),
).toEqual([
'applySafe',
'overridableConfigFlags',
'pluginNames',
'schedules',
'sourceKinds',
'trustedSources',
]);
});

it.each(cases.trustedSources.cases)(
'MatchTrustedSource(%j)',
(source, want) => {
expect(sourceTrusted(cases.trustedSources.patterns, source)).toBe(want);
},
);

it.each(cases.trustedSources.extra)(
'MatchTrustedSource($patterns, $source)',
({ patterns, source, want }) => {
expect(sourceTrusted(patterns, source)).toBe(want);
},
);

it.each(cases.overridableConfigFlags)(
'MatchOverridableConfigFlag: $name',
({ flags, plugin, key, want }) => {
expect(configKeyOverridable(flags, plugin, key)).toBe(want);
},
);

it.each(cases.sourceKinds)('KindOf(%j) = %s', (source, kind) => {
expect(sourceKind(source)).toBe(kind);
});

it.each(cases.schedules.valid)('ParseSchedule(%j) succeeds', (expr) => {
expect(validateCron5(expr)).toBeNull();
});

it.each(cases.schedules.invalid)('ParseSchedule(%j) fails', (expr) => {
expect(validateCron5(expr)).not.toBeNull();
});

it.each(cases.applySafe.cases)(
'apply_safe: $name',
({ trusted, file, overlay, path, state }) => {
const inst = applySafe(trusted);
const base: ConfigDoc = { plugins: file };
expect(
fieldAccess(path, {
instances: [inst],
bases: new Map([[inst.instanceId, base]]),
overlay,
}).state,
).toBe(state);
},
);
});
Loading
Loading