From cb17163f7a5513cb0850feed82a959600d0f0864 Mon Sep 17 00:00:00 2001 From: Yehonal Date: Thu, 4 Jun 2026 14:43:17 +0000 Subject: [PATCH] Add manifest boundary fixture validation --- package.json | 3 +- ...client-project-boundary.expected-fail.json | 46 ++++ .../core-neutral.valid.json | 37 ++++ ...elopment-agent-boundary.expected-fail.json | 49 +++++ ...vate-assistant-boundary.expected-fail.json | 45 ++++ ...usiness-domain-boundary.expected-fail.json | 45 ++++ ...taff-community-boundary.expected-fail.json | 50 +++++ packages/core/package.json | 8 +- .../core/scripts/verify-manifest-fixtures.mjs | 47 +++++ packages/core/src/manifest.js | 199 ++++++++++++++++++ packages/core/test/manifest-boundary.test.js | 134 ++++++++++++ 11 files changed, 659 insertions(+), 4 deletions(-) create mode 100644 packages/core/fixtures/manifest-boundaries/client-project-boundary.expected-fail.json create mode 100644 packages/core/fixtures/manifest-boundaries/core-neutral.valid.json create mode 100644 packages/core/fixtures/manifest-boundaries/development-agent-boundary.expected-fail.json create mode 100644 packages/core/fixtures/manifest-boundaries/private-assistant-boundary.expected-fail.json create mode 100644 packages/core/fixtures/manifest-boundaries/sensitive-business-domain-boundary.expected-fail.json create mode 100644 packages/core/fixtures/manifest-boundaries/staff-community-boundary.expected-fail.json create mode 100644 packages/core/scripts/verify-manifest-fixtures.mjs create mode 100644 packages/core/src/manifest.js create mode 100644 packages/core/test/manifest-boundary.test.js diff --git a/package.json b/package.json index 68481b1..2ff515b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "npm run build --workspaces --if-present", "typecheck": "npm run typecheck --workspaces --if-present", "test": "npm run test --workspaces --if-present", - "verify": "npm run build && npm test && npm run mesh:smoke && npm run mesh:decentralized && npm run mesh:readiness && npm run mesh:hydrate", + "manifest:fixtures": "npm --workspace packages/core run manifest:fixtures", + "verify": "npm run build && npm test && npm run manifest:fixtures && npm run mesh:smoke && npm run mesh:decentralized && npm run mesh:readiness && npm run mesh:hydrate", "lint": "npm run build", "mesh:readiness": "node scripts/mesh-readiness.mjs", "mesh:harness": "node scripts/mesh-dispatch-harness.mjs", diff --git a/packages/core/fixtures/manifest-boundaries/client-project-boundary.expected-fail.json b/packages/core/fixtures/manifest-boundaries/client-project-boundary.expected-fail.json new file mode 100644 index 0000000..e2c7f9f --- /dev/null +++ b/packages/core/fixtures/manifest-boundaries/client-project-boundary.expected-fail.json @@ -0,0 +1,46 @@ +{ + "owner": "synthetic-client-domain", + "boundary_class": "client-project-boundary", + "threat": "client-project internal context leaks into publishable/runtime-neutral artifacts, generated runtime config, audit output, or unauthorized fallback summaries", + "surfaces": [ + "generated_prompt", + "generated_config", + "repo_artifact", + "audit_log", + "public_channel", + "terminal_adapter", + "fallback_summary" + ], + "data_boundary": { + "forbidden": [ + "client_internal_context", + "customer/project-private names", + "internal repository details", + "live runtime bindings", + "numeric platform IDs", + "host paths/secrets" + ] + }, + "egress_policy": "private client-project data must not appear in repo fixtures, generated prompts/configs, logs, public/terminal surfaces, or fallback summaries", + "expected_result": "fail validation with deterministic client-project-boundary error; include field/path, never raw private value", + "negative_cases": [ + { + "id": "client_internal_context_in_generated_config", + "surface": "generated_config", + "failure_mode": "client_internal_context appears in generated runtime config", + "expected": "fail" + }, + { + "id": "unauthorized_fallback_summary", + "surface": "fallback_summary", + "failure_mode": "client-project details are summarized to an unauthorized domain agent through fallback", + "expected": "fail" + } + ], + "artifact": { + "generated_config": { + "client_context_path": "C:\\private\\client\\runtime.json" + }, + "fallback_summary": "credential: synthetic-secret-token" + } +} diff --git a/packages/core/fixtures/manifest-boundaries/core-neutral.valid.json b/packages/core/fixtures/manifest-boundaries/core-neutral.valid.json new file mode 100644 index 0000000..7d51197 --- /dev/null +++ b/packages/core/fixtures/manifest-boundaries/core-neutral.valid.json @@ -0,0 +1,37 @@ +{ + "owner": "public-fixture-suite", + "boundary_class": "core-neutral", + "threat": "public repo fixtures accidentally include private or live operational identifiers", + "surfaces": [ + "repo_fixture", + "generated_prompt", + "generated_config", + "ci_log" + ], + "data_boundary": { + "forbidden": [ + "live platform IDs", + "host paths", + "token-shaped strings", + "local deployment identifiers" + ] + }, + "egress_policy": "repo fixtures and CI output must remain synthetic and runtime-neutral", + "expected_result": "pass when fixture and generated artifact contain only synthetic runtime-neutral data", + "negative_cases": [ + { + "id": "public_fixture_contains_live_identifier", + "surface": "repo_fixture", + "failure_mode": "fixture contains live identifier, host path, token-shaped string, or local deployment identifier", + "expected": "fail" + } + ], + "artifact": { + "schema": "agent-manifest/v0", + "owner": "public-fixture-suite", + "generated_config": { + "transport": "runtime-neutral", + "route": "synthetic-example" + } + } +} diff --git a/packages/core/fixtures/manifest-boundaries/development-agent-boundary.expected-fail.json b/packages/core/fixtures/manifest-boundaries/development-agent-boundary.expected-fail.json new file mode 100644 index 0000000..5471612 --- /dev/null +++ b/packages/core/fixtures/manifest-boundaries/development-agent-boundary.expected-fail.json @@ -0,0 +1,49 @@ +{ + "owner": "synthetic-development-agent-domain", + "boundary_class": "development-agent-boundary", + "threat": "development-agent runtime details, routing metadata, or local workspace state leak into publishable manifest artifacts", + "surfaces": [ + "generated_prompt", + "generated_config", + "repo_artifact", + "audit_log", + "public_channel", + "terminal_adapter", + "fallback_summary" + ], + "data_boundary": { + "forbidden": [ + "runtime-specific agent names", + "routing metadata", + "local workspace paths", + "live terminal/session identifiers", + "numeric platform IDs", + "host paths", + "secrets" + ] + }, + "egress_policy": "development-agent manifests must stay runtime-neutral and must not expose local routing, workspace, terminal, or live-operation details", + "expected_result": "fail validation with deterministic development-agent-boundary error; include field/path, never raw private value", + "negative_cases": [ + { + "id": "local_workspace_path_in_generated_config", + "surface": "generated_config", + "failure_mode": "local workspace path appears in generated runtime config", + "expected": "fail" + }, + { + "id": "live_route_identifier_in_audit_log", + "surface": "audit_log", + "failure_mode": "live route or platform identifier appears in audit log", + "expected": "fail" + } + ], + "artifact": { + "generated_config": { + "workspace_root": "/private/workspace/development-agent" + }, + "audit_log": { + "route_marker": "synthetic-live-route" + } + } +} diff --git a/packages/core/fixtures/manifest-boundaries/private-assistant-boundary.expected-fail.json b/packages/core/fixtures/manifest-boundaries/private-assistant-boundary.expected-fail.json new file mode 100644 index 0000000..04c0a02 --- /dev/null +++ b/packages/core/fixtures/manifest-boundaries/private-assistant-boundary.expected-fail.json @@ -0,0 +1,45 @@ +{ + "owner": "synthetic-private-assistant-domain", + "boundary_class": "private-assistant-boundary", + "threat": "private assistant persona, memory, or personal context leaks into publishable manifest artifacts or runtime outputs", + "surfaces": [ + "generated_prompt", + "generated_config", + "repo_artifact", + "audit_log", + "public_channel", + "terminal_adapter", + "fallback_summary" + ], + "data_boundary": { + "forbidden": [ + "private assistant memory", + "persona-private context", + "personal relationship context", + "live runtime bindings", + "numeric platform IDs", + "host paths", + "secrets" + ] + }, + "egress_policy": "private assistant data must not appear in repo fixtures, generated prompts/configs, logs, public/terminal surfaces, or fallback summaries", + "expected_result": "fail validation with deterministic private-assistant-boundary error; include field/path, never raw private value", + "negative_cases": [ + { + "id": "private_assistant_memory_in_generated_prompt", + "surface": "generated_prompt", + "failure_mode": "private assistant memory appears in a generated prompt", + "expected": "fail" + }, + { + "id": "persona_context_in_fallback_summary", + "surface": "fallback_summary", + "failure_mode": "persona-private context is summarized into an unauthorized fallback surface", + "expected": "fail" + } + ], + "artifact": { + "generated_prompt": "load assistant context from /private/assistant/memory", + "fallback_summary": "private_key: synthetic-secret-token" + } +} diff --git a/packages/core/fixtures/manifest-boundaries/sensitive-business-domain-boundary.expected-fail.json b/packages/core/fixtures/manifest-boundaries/sensitive-business-domain-boundary.expected-fail.json new file mode 100644 index 0000000..8f4d70c --- /dev/null +++ b/packages/core/fixtures/manifest-boundaries/sensitive-business-domain-boundary.expected-fail.json @@ -0,0 +1,45 @@ +{ + "owner": "synthetic-business-domain", + "boundary_class": "sensitive-business-domain-boundary", + "threat": "sensitive business context leaks into generated runtime artifacts or audit outputs", + "surfaces": [ + "generated_prompt", + "generated_config", + "public_channel", + "terminal_adapter", + "audit_log", + "fallback_summary" + ], + "data_boundary": { + "forbidden": [ + "private_business_memory", + "raw business context", + "live runtime bindings", + "numeric platform IDs", + "host paths", + "secrets" + ] + }, + "egress_policy": "private business data must not appear in generated prompts/configs, logs, public channels, terminal adapters, or fallback summaries", + "expected_result": "fail validation with deterministic sensitive-business-domain-boundary error; include field/path, never raw private value", + "negative_cases": [ + { + "id": "sensitive_business_memory_in_generated_prompt", + "surface": "generated_prompt", + "failure_mode": "private_business_memory appears in generated prompt", + "expected": "fail" + }, + { + "id": "raw_business_context_in_audit_log", + "surface": "audit_log", + "failure_mode": "raw private business context appears in audit log instead of sanitized reason code", + "expected": "fail" + } + ], + "artifact": { + "generated_prompt": "load context from /private/business/context", + "audit_log": { + "reason": "live-secret-token" + } + } +} diff --git a/packages/core/fixtures/manifest-boundaries/staff-community-boundary.expected-fail.json b/packages/core/fixtures/manifest-boundaries/staff-community-boundary.expected-fail.json new file mode 100644 index 0000000..196ff12 --- /dev/null +++ b/packages/core/fixtures/manifest-boundaries/staff-community-boundary.expected-fail.json @@ -0,0 +1,50 @@ +{ + "owner": "synthetic-staff-community-domain", + "boundary_class": "staff-community-boundary", + "threat": "staff or community operational context leaks into publishable manifest artifacts, generated runtime config, audit output, or fallback summaries", + "surfaces": [ + "generated_prompt", + "generated_config", + "repo_artifact", + "audit_log", + "public_channel", + "terminal_adapter", + "fallback_summary" + ], + "data_boundary": { + "forbidden": [ + "real community names", + "staff structure", + "organization-internal roles", + "ownership details", + "runtime topology", + "numeric platform IDs", + "host paths", + "secrets" + ] + }, + "egress_policy": "staff/community data must be synthetic only and must not expose real community, staff, organization, runtime, ownership, routing, ID, path, topology, transcript, or log details", + "expected_result": "fail validation with deterministic staff-community-boundary error; include field/path, never raw private value", + "negative_cases": [ + { + "id": "staff_context_in_repo_artifact", + "surface": "repo_artifact", + "failure_mode": "staff/community context appears in a repo-facing artifact", + "expected": "fail" + }, + { + "id": "community_runtime_id_in_generated_config", + "surface": "generated_config", + "failure_mode": "community runtime identifier appears in generated config", + "expected": "fail" + } + ], + "artifact": { + "repo_artifact": { + "notes": "load staff context from /private/community/staff" + }, + "generated_config": { + "community_route_marker": "synthetic-live-route" + } + } +} diff --git a/packages/core/package.json b/packages/core/package.json index f4f37b7..0431b8b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -5,13 +5,15 @@ "type": "module", "description": "Runtime-agnostic Agent Mesh policy, task-turn, run-scope, and replay-control primitives.", "scripts": { - "build": "node --check src/policy.js", - "test": "node --test" + "build": "node --check src/policy.js && node --check src/manifest.js", + "test": "node --test", + "manifest:fixtures": "node scripts/verify-manifest-fixtures.mjs" }, "main": "./src/policy.js", "exports": { ".": "./src/policy.js", - "./policy": "./src/policy.js" + "./policy": "./src/policy.js", + "./manifest": "./src/manifest.js" }, "files": [ "src", diff --git a/packages/core/scripts/verify-manifest-fixtures.mjs b/packages/core/scripts/verify-manifest-fixtures.mjs new file mode 100644 index 0000000..e43dade --- /dev/null +++ b/packages/core/scripts/verify-manifest-fixtures.mjs @@ -0,0 +1,47 @@ +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; + +import { + runManifestBoundaryFixture, + validateManifestBoundaryFixture, + validateRequiredFixtureClasses +} from "../src/manifest.js"; + +const fixtureDir = path.resolve(import.meta.dirname, "../fixtures/manifest-boundaries"); +const entries = (await readdir(fixtureDir)).filter((entry) => entry.endsWith(".json")).sort(); +const fixtures = []; +const failures = []; + +for (const entry of entries) { + const fixture = JSON.parse(await readFile(path.join(fixtureDir, entry), "utf8")); + fixtures.push(fixture); + + const schemaResult = validateManifestBoundaryFixture(fixture); + if (!schemaResult.ok) { + failures.push({ file: entry, phase: "schema", issues: schemaResult.issues }); + continue; + } + + const artifact = fixture.artifact ?? {}; + const runResult = runManifestBoundaryFixture(fixture, artifact); + if (entry.endsWith(".expected-fail.json")) { + if (runResult.ok) { + failures.push({ file: entry, phase: "expected-fail", issues: [{ message: "fixture artifact unexpectedly passed" }] }); + } + } else if (!runResult.ok) { + failures.push({ file: entry, phase: "run", issues: runResult.issues }); + } +} + +const requiredResult = validateRequiredFixtureClasses(fixtures); +if (!requiredResult.ok) { + failures.push({ file: "", phase: "required-classes", issues: requiredResult.issues }); +} + +if (failures.length) { + console.error(JSON.stringify({ ok: false, failures }, null, 2)); + process.exitCode = 1; +} else { + console.log(JSON.stringify({ ok: true, fixtures: entries }, null, 2)); +} diff --git a/packages/core/src/manifest.js b/packages/core/src/manifest.js new file mode 100644 index 0000000..0c9b31a --- /dev/null +++ b/packages/core/src/manifest.js @@ -0,0 +1,199 @@ +const DISCORD_ID_PATTERN = /(?|<#\d{17,20}>|<@&\d{17,20}>/; +const LOCAL_HOST_PATH_PATTERN = /(?:^|[\s"'=:(])(?:\/[A-Za-z0-9._-]+){2,}/; +const WINDOWS_HOST_PATH_PATTERN = /[A-Za-z]:\\(?:[^\\\s]+\\)+[^\\\s]*/; +const SECRET_LIKE_KEY_PATTERN = /(?:secret|token|credential|password|api[_-]?key|private[_-]?key|mcp[_-]?server)/i; + +const DEFAULT_REQUIRED_FIXTURE_CLASSES = [ + "core-neutral", + "sensitive-business-domain-boundary", + "client-project-boundary", + "private-assistant-boundary", + "development-agent-boundary", + "staff-community-boundary" +]; + +export function validateManifestBoundaryFixture(input) { + const issues = []; + if (!isRecord(input)) { + return fail([{ path: "$", message: "must be an object" }]); + } + + const owner = requireString(input, "owner", issues); + const boundary_class = requireString(input, "boundary_class", issues); + const threat = requireString(input, "threat", issues); + const surfaces = requireStringArray(input, "surfaces", issues); + const data_boundary = validateDataBoundary(input.data_boundary, issues); + const egress_policy = requireString(input, "egress_policy", issues); + const expected_result = requireString(input, "expected_result", issues); + const negative_cases = validateNegativeCases(input.negative_cases, issues); + + if (issues.length) return fail(issues); + + return ok({ + owner, + boundary_class, + threat, + surfaces, + data_boundary, + egress_policy, + expected_result, + negative_cases + }); +} + +export function validateRequiredFixtureClasses(fixtures, requiredClasses = DEFAULT_REQUIRED_FIXTURE_CLASSES) { + const issues = []; + if (!Array.isArray(fixtures)) { + return fail([{ path: "$", code: "invalid_fixture_collection", message: "must be an array of fixtures" }]); + } + + const classes = new Set(); + fixtures.forEach((fixture, index) => { + const result = validateManifestBoundaryFixture(fixture); + if (!result.ok) { + for (const issue of result.issues) { + issues.push({ ...issue, path: `fixtures[${index}].${issue.path}`, code: "invalid_boundary_fixture", boundary_class: safeBoundaryClass(fixture) }); + } + return; + } + classes.add(result.value.boundary_class); + }); + + for (const requiredClass of requiredClasses) { + if (!classes.has(requiredClass)) { + issues.push({ path: "$", code: "missing_required_fixture_class", boundary_class: requiredClass, message: `missing required fixture class ${requiredClass}` }); + } + } + + if (issues.length) return fail(issues); + return ok({ requiredClasses, foundClasses: [...classes].sort() }); +} + +export function runManifestBoundaryFixture(fixtureInput, artifact) { + const fixtureResult = validateManifestBoundaryFixture(fixtureInput); + if (!fixtureResult.ok) { + return fail( + fixtureResult.issues.map((issue) => ({ + ...issue, + code: "invalid_boundary_fixture", + boundary_class: safeBoundaryClass(fixtureInput) + })) + ); + } + + const fixture = fixtureResult.value; + const findings = scanForBoundaryViolations(artifact); + if (findings.length === 0) return ok({ boundary_class: fixture.boundary_class }); + + return fail( + findings.map((finding) => ({ + path: finding.path, + code: "privacy_boundary_violation", + boundary_class: fixture.boundary_class, + message: `${fixture.boundary_class} rejected ${finding.kind} at ${finding.path}` + })) + ); +} + +export function scanForBoundaryViolations(value, basePath = "$") { + const findings = []; + walkJsonLike(value, basePath, (path, node, key) => { + if (typeof node !== "string") return; + + if (DISCORD_MENTION_PATTERN.test(node)) { + findings.push({ path, kind: "discord_id" }); + return; + } + if (DISCORD_ID_PATTERN.test(node)) { + findings.push({ path, kind: "discord_id" }); + return; + } + if (LOCAL_HOST_PATH_PATTERN.test(node) || WINDOWS_HOST_PATH_PATTERN.test(node)) { + findings.push({ path, kind: "local_host_path" }); + return; + } + if (SECRET_LIKE_KEY_PATTERN.test(String(key ?? "")) || SECRET_LIKE_KEY_PATTERN.test(node)) { + findings.push({ path, kind: "secret_or_live_operation_identifier" }); + } + }); + return findings; +} + +function validateDataBoundary(value, issues) { + if (!isRecord(value)) { + issues.push({ path: "data_boundary", message: "must be an object" }); + return undefined; + } + const forbidden = requireStringArray(value, "forbidden", issues, "data_boundary.forbidden"); + return { forbidden }; +} + +function validateNegativeCases(value, issues) { + if (!Array.isArray(value) || value.length === 0) { + issues.push({ path: "negative_cases", message: "must be a non-empty array" }); + return undefined; + } + + const cases = []; + for (const [index, item] of value.entries()) { + const path = `negative_cases[${index}]`; + if (!isRecord(item)) { + issues.push({ path, message: "must be an object" }); + continue; + } + const id = requireString(item, "id", issues, `${path}.id`); + const surface = requireString(item, "surface", issues, `${path}.surface`); + const failure_mode = requireString(item, "failure_mode", issues, `${path}.failure_mode`); + const expected = requireString(item, "expected", issues, `${path}.expected`); + cases.push({ id, surface, failure_mode, expected }); + } + return cases; +} + +function requireString(input, key, issues, path = key) { + const value = input[key]; + if (typeof value !== "string" || value.length === 0) { + issues.push({ path, message: "must be a non-empty string" }); + return undefined; + } + return value; +} + +function requireStringArray(input, key, issues, path = key) { + const value = input[key]; + if (!Array.isArray(value) || value.length === 0 || !value.every((item) => typeof item === "string" && item.length > 0)) { + issues.push({ path, message: "must be a non-empty array of non-empty strings" }); + return undefined; + } + return value; +} + +function walkJsonLike(value, path, visit, key) { + visit(path, value, key); + if (Array.isArray(value)) { + value.forEach((item, index) => walkJsonLike(item, `${path}[${index}]`, visit, index)); + return; + } + if (isRecord(value)) { + for (const [childKey, childValue] of Object.entries(value)) { + walkJsonLike(childValue, path === "$" ? childKey : `${path}.${childKey}`, visit, childKey); + } + } +} + +function safeBoundaryClass(value) { + return isRecord(value) && typeof value.boundary_class === "string" ? value.boundary_class : "unknown"; +} + +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function ok(value) { + return { ok: true, value, issues: [] }; +} + +function fail(issues) { + return { ok: false, issues }; +} diff --git a/packages/core/test/manifest-boundary.test.js b/packages/core/test/manifest-boundary.test.js new file mode 100644 index 0000000..823f982 --- /dev/null +++ b/packages/core/test/manifest-boundary.test.js @@ -0,0 +1,134 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + runManifestBoundaryFixture, + validateManifestBoundaryFixture, + validateRequiredFixtureClasses +} from "../src/manifest.js"; + +const minimumFixture = { + owner: "example-domain", + boundary_class: "example-boundary", + threat: "private operational context leaking into publishable manifest artifacts", + surfaces: ["manifest fixture export", "validator error output", "CI logs"], + data_boundary: { + forbidden: [ + "customer/staff personal context", + "private operational memory", + "numeric platform IDs", + "local host paths", + "server names or credentials tied to live operations" + ] + }, + egress_policy: "no private data may appear in repo fixtures, generated manifests, validator snapshots, or CI output", + expected_result: "fail validation with deterministic privacy-boundary error", + negative_cases: [ + { + id: "private-memory-egress", + surface: "manifest fixture export", + failure_mode: "fixture includes private operational memory or identifying runtime metadata", + expected: "validator rejects; output references field/path and boundary class, without echoing the private value" + } + ] +}; + +test("validates the minimum manifest boundary fixture schema", () => { + const result = validateManifestBoundaryFixture(minimumFixture); + + assert.equal(result.ok, true); + assert.equal(result.value.boundary_class, "example-boundary"); + assert.equal(result.value.negative_cases[0].id, "private-memory-egress"); +}); + +test("requires deterministic negative cases", () => { + const result = validateManifestBoundaryFixture({ + ...minimumFixture, + negative_cases: [] + }); + + assert.equal(result.ok, false); + assert.deepEqual(result.issues.map((issue) => issue.path), ["negative_cases"]); +}); + +test("requires every negative case field", () => { + const result = validateManifestBoundaryFixture({ + ...minimumFixture, + negative_cases: [{ id: "missing-fields", surface: "generated_config", expected: "fail" }] + }); + + assert.equal(result.ok, false); + assert.deepEqual(result.issues.map((issue) => issue.path), ["negative_cases[0].failure_mode"]); +}); + +test("rejects private identifiers without echoing private values", () => { + const numericId = "1".repeat(18); + const hostPath = `/${["private", "workspace", "customer-prod"].join("/")}`; + const credentialValue = ["live", "credential"].join("-"); + const artifact = { + manifest: { + owner: "example-domain", + operational_note: `deploy from ${hostPath}`, + channel: numericId, + mention: `<@${numericId}>`, + credential: credentialValue + } + }; + + const result = runManifestBoundaryFixture(minimumFixture, artifact); + + assert.equal(result.ok, false); + assert.equal(result.issues[0].code, "privacy_boundary_violation"); + assert.equal(result.issues[0].boundary_class, "example-boundary"); + assert.match(result.issues.map((issue) => issue.path).join("\n"), /manifest\.operational_note/); + assert.match(result.issues.map((issue) => issue.path).join("\n"), /manifest\.channel/); + assert.match(result.issues.map((issue) => issue.path).join("\n"), /manifest\.mention/); + assert.match(result.issues.map((issue) => issue.path).join("\n"), /manifest\.credential/); + assert.doesNotMatch(JSON.stringify(result), /customer-prod/); + assert.doesNotMatch(JSON.stringify(result), new RegExp(numericId)); + assert.doesNotMatch(JSON.stringify(result), new RegExp(credentialValue)); +}); + +test("accepts runtime-neutral artifacts with no boundary findings", () => { + const result = runManifestBoundaryFixture(minimumFixture, { + schema: "agent-manifest/v0", + owner: "example-domain", + manifest: { + capabilities: ["handoff", "validation"], + transport: "runtime-neutral" + } + }); + + assert.equal(result.ok, true); + assert.deepEqual(result.issues, []); +}); + +test("requires the public Sprint 0 fixture classes", () => { + const fixtures = [ + { ...minimumFixture, boundary_class: "core-neutral" }, + { ...minimumFixture, boundary_class: "sensitive-business-domain-boundary" }, + { ...minimumFixture, boundary_class: "client-project-boundary" }, + { ...minimumFixture, boundary_class: "private-assistant-boundary" }, + { ...minimumFixture, boundary_class: "development-agent-boundary" }, + { ...minimumFixture, boundary_class: "staff-community-boundary" } + ]; + + const result = validateRequiredFixtureClasses(fixtures); + + assert.equal(result.ok, true); + assert.deepEqual(result.value.requiredClasses, [ + "core-neutral", + "sensitive-business-domain-boundary", + "client-project-boundary", + "private-assistant-boundary", + "development-agent-boundary", + "staff-community-boundary" + ]); +}); + +test("fails closed when a required fixture class is absent", () => { + const result = validateRequiredFixtureClasses([{ ...minimumFixture, boundary_class: "core-neutral" }]); + + assert.equal(result.ok, false); + assert.equal(result.issues.some((issue) => issue.boundary_class === "staff-community-boundary"), true); +});