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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
- Pending approvals now have an explicit mixed-risk ordering test and panel-side sort to keep critical items pinned before older lower-priority reviews.
- The approvals panel now shows a queue health summary with pending count, critical count, and oldest waiting item before the pending decision list.
- Brand asset docs now point at the actual public asset path.
- Policy and config YAML now parses under the YAML 1.2 core schema on js-yaml 5. Merge keys (`<<`) are no longer expanded, so a policy file that relies on one is rejected whole and the last good ruleset stays in force instead of a partially assembled rule taking effect. Unquoted dates load as strings rather than `Date` objects, and a mapping with a complex key is rejected instead of having that key flattened into a lossy string.
- The dashboard runtime-context panel now degrades to "none" when the agent harness config file cannot be parsed, rather than failing the whole dashboard state build on a file Agentwall does not own.

## [0.1.0] - 2026-03-23

Expand Down
139 changes: 46 additions & 93 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,12 @@
"license": "Apache-2.0",
"dependencies": {
"fastify": "5.11.0",
"js-yaml": "^4.1.0",
"js-yaml": "5.2.3",
"pino": "^8.19.0",
"zod": "4.4.3"
},
"devDependencies": {
"@jest/globals": "^30.4.1",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.11.5",
"jest": "^30.4.2",
"ts-jest": "^29.4.12",
Expand Down
14 changes: 12 additions & 2 deletions src/dashboard/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,21 @@ function yamlTopLevelKeys(source: unknown): string[] {
}

function summarizeYamlSource(absolutePath: string): { keys: string[]; modelDefault: string | null; modelProvider: string | null; skin: string | null } {
const unknownSummary = { keys: [] as string[], modelDefault: null, modelProvider: null, skin: null };
const text = readTextSafely(absolutePath);
if (!text) {
return { keys: [], modelDefault: null, modelProvider: null, skin: null };
return unknownSummary;
}
// This file belongs to the agent harness, not to us, so it is foreign input: it can hold
// nothing but comments, be half-written while an editor saves it, or be deliberately
// malformed by whoever can write into the agent home. A parse failure degrades one
// dashboard panel to "unknown" instead of taking the whole state build down with it.
let parsed: unknown;
try {
parsed = yaml.load(text);
} catch {
return unknownSummary;
}
const parsed = yaml.load(text);
return {
keys: yamlTopLevelKeys(parsed),
modelDefault: nestedStringValue(parsed, ["model", "default"]),
Expand Down
55 changes: 55 additions & 0 deletions tests/dashboard-harness-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { afterAll, describe, expect, it } from "@jest/globals";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";

/**
* The dashboard reads the agent harness's own config.yaml to describe the runtime context.
* That file belongs to the harness, not to Agentwall, so its bytes are foreign input: it can
* hold nothing but comments, be caught half-written while an editor saves it, or be
* deliberately malformed by anyone who can write into the agent home. A YAML parse failure
* there costs one panel its detail and nothing more, because an attacker who can blind the
* whole operator view can then act unobserved.
*/
const agentHome = fs.mkdtempSync(path.join(os.tmpdir(), "agentwall-harness-home-"));
process.env["AGENTWALL_AGENT_HOME"] = agentHome;

afterAll(() => {
fs.rmSync(agentHome, { recursive: true, force: true });
});

// Loaded through await import, not a static import, because state.ts resolves
// AGENTWALL_AGENT_HOME once at module load and static imports are evaluated before the
// assignment above runs. This test exercises that module-loading boundary on purpose.
async function snapshotFacts(harnessConfig: string): Promise<Array<{ label: string; value: string }>> {
fs.writeFileSync(path.join(agentHome, "config.yaml"), harnessConfig);
const { RuntimeState } = await import("../src/dashboard/state");
const { loadConfig } = await import("../src/config");
const snapshot = new RuntimeState(loadConfig("examples/config.yaml")).getSnapshot(0);
const entry = snapshot.knowledgeBase.entries.find((item) => item.id === "system_environment");
expect(entry).toBeDefined();
return entry?.facts ?? [];
}

describe("dashboard runtime context with an unreadable harness config", () => {
it.each([
["a document with no content", "# nothing configured yet\n"],
["unparseable YAML", "model:\n\tdefault: gpt\n"],
["a truncated flow collection", "model: [\n"],
])("degrades one panel when the harness config is %s", async (_label, contents) => {
const facts = await snapshotFacts(contents);

// The file is present and was read; only the parse of it failed.
expect(facts.find((fact) => fact.label === "Config file")?.value).toBe("configured");
expect(facts.find((fact) => fact.label === "Config keys")?.value).toBe("none");
expect(facts.find((fact) => fact.label === "Model")?.value).toBe("unknown");
});

it("still reports keys when the harness config parses", async () => {
const facts = await snapshotFacts("model:\n default: gpt-5\ndisplay:\n skin: dark\n");

expect(facts.find((fact) => fact.label === "Config keys")?.value).toBe("model, display");
expect(facts.find((fact) => fact.label === "Model")?.value).toBe("gpt-5");
expect(facts.find((fact) => fact.label === "Display skin")?.value).toBe("dark");
});
});
Loading