feat: define workspace evidence descriptor contract
This commit is contained in:
@@ -104,7 +104,52 @@ interface QdrantVectorStore {
|
||||
distance: "cosine";
|
||||
}
|
||||
|
||||
export interface WorkspaceV3 extends WorkspaceBase<3, QdrantVectorStore> {}
|
||||
export interface EvidencePolicy {
|
||||
max_chunk_chars: number;
|
||||
retain_published_generations: number;
|
||||
}
|
||||
|
||||
export type EvidenceSource =
|
||||
| {
|
||||
type: "filesystem";
|
||||
uri: string;
|
||||
patterns: string[];
|
||||
max_bytes: number;
|
||||
}
|
||||
| {
|
||||
type: "http";
|
||||
uris: string[];
|
||||
authentication: "none" | "signed_urls_file";
|
||||
connect_timeout_ms: number;
|
||||
read_timeout_ms: number;
|
||||
max_bytes: number;
|
||||
max_redirects: number;
|
||||
allow_private_hosts: boolean;
|
||||
max_cache_bytes: number;
|
||||
}
|
||||
| {
|
||||
type: "s3";
|
||||
uri: string;
|
||||
endpoint_url?: string;
|
||||
region?: string;
|
||||
credentials: "ambient" | "static_files";
|
||||
trusted_endpoint: boolean;
|
||||
allow_private_endpoint: boolean;
|
||||
allow_insecure_endpoint: boolean;
|
||||
max_bytes: number;
|
||||
max_objects: number;
|
||||
max_pages: number;
|
||||
page_size: number;
|
||||
};
|
||||
|
||||
export interface WorkspaceEvidence {
|
||||
source: EvidenceSource;
|
||||
policy: EvidencePolicy;
|
||||
}
|
||||
|
||||
export interface WorkspaceV3 extends WorkspaceBase<3, QdrantVectorStore> {
|
||||
evidence?: WorkspaceEvidence;
|
||||
}
|
||||
export interface WorkspaceV2 extends WorkspaceBase<2, VectorStore & { database: string; schema: string }> {}
|
||||
|
||||
/** A readable, non-operational v1 descriptor. It must be explicitly migrated before use. */
|
||||
@@ -218,6 +263,147 @@ const llmPolicySchema = z.object({
|
||||
allowed: z.array(modelReference).min(1),
|
||||
}).strict();
|
||||
|
||||
const positiveSafeInteger = z.number().int().safe().positive();
|
||||
const nonnegativeSafeInteger = z.number().int().safe().nonnegative();
|
||||
|
||||
function isSafeEvidencePattern(value: string): boolean {
|
||||
const parts = value.split("/");
|
||||
return value.length > 0
|
||||
&& !value.startsWith("/")
|
||||
&& !value.includes("\\")
|
||||
&& !/[\u0000-\u001f\u007f]/u.test(value)
|
||||
&& parts.every((part) => part !== "" && part !== "." && part !== "..");
|
||||
}
|
||||
|
||||
function parsePublicHttpUri(value: string): URL | undefined {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (
|
||||
!["http:", "https:"].includes(parsed.protocol)
|
||||
|| parsed.hostname.length === 0
|
||||
|| parsed.username !== ""
|
||||
|| parsed.password !== ""
|
||||
|| parsed.search !== ""
|
||||
|| parsed.hash !== ""
|
||||
) return undefined;
|
||||
return parsed;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function canonicalPublicHttpUri(value: string): string | undefined {
|
||||
return parsePublicHttpUri(value)?.href;
|
||||
}
|
||||
|
||||
function isSafeS3Uri(value: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
return parsed.protocol === "s3:"
|
||||
&& parsed.hostname.length > 0
|
||||
&& parsed.username === ""
|
||||
&& parsed.password === ""
|
||||
&& parsed.search === ""
|
||||
&& parsed.hash === "";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isSafeS3Endpoint(value: string): boolean {
|
||||
const parsed = parsePublicHttpUri(value);
|
||||
return parsed !== undefined && (parsed.pathname === "/" || parsed.pathname === "");
|
||||
}
|
||||
|
||||
const evidencePattern = z.string().refine(isSafeEvidencePattern, {
|
||||
message: "evidence patterns must be normalized relative globs",
|
||||
});
|
||||
const filesystemEvidenceSourceSchema = z.object({
|
||||
type: z.literal("filesystem"),
|
||||
uri: z.string(),
|
||||
patterns: z.array(evidencePattern).min(1).default(["**/*.md"]),
|
||||
max_bytes: positiveSafeInteger.default(10 * 1024 * 1024),
|
||||
}).strict().superRefine((source, context) => {
|
||||
if (new Set(source.patterns).size !== source.patterns.length) {
|
||||
context.addIssue({ code: "custom", path: ["patterns"], message: "evidence patterns must not repeat" });
|
||||
}
|
||||
});
|
||||
const httpEvidenceUri = z.string().refine((value) => parsePublicHttpUri(value) !== undefined, {
|
||||
message: "HTTP evidence URIs must be public http(s) identities without credentials, query, or fragment",
|
||||
});
|
||||
const httpEvidenceSourceSchema = z.object({
|
||||
type: z.literal("http"),
|
||||
uris: z.array(httpEvidenceUri).min(1),
|
||||
authentication: z.enum(["none", "signed_urls_file"]).default("none"),
|
||||
connect_timeout_ms: positiveSafeInteger.default(5_000),
|
||||
read_timeout_ms: positiveSafeInteger.default(30_000),
|
||||
max_bytes: positiveSafeInteger.default(10 * 1024 * 1024),
|
||||
max_redirects: nonnegativeSafeInteger.default(5),
|
||||
allow_private_hosts: z.boolean().default(false),
|
||||
max_cache_bytes: positiveSafeInteger.default(64 * 1024 * 1024),
|
||||
}).strict().superRefine((source, context) => {
|
||||
const canonical = source.uris.map(canonicalPublicHttpUri);
|
||||
if (new Set(canonical).size !== canonical.length) {
|
||||
context.addIssue({ code: "custom", path: ["uris"], message: "HTTP evidence URIs must not repeat" });
|
||||
}
|
||||
});
|
||||
const s3EvidenceSourceSchema = z.object({
|
||||
type: z.literal("s3"),
|
||||
uri: z.string().refine(isSafeS3Uri, {
|
||||
message: "S3 evidence URI must use s3:// without credentials, query, or fragment",
|
||||
}),
|
||||
endpoint_url: z.string().refine(isSafeS3Endpoint, {
|
||||
message: "S3 endpoint must be an origin-only http(s) URL without credentials",
|
||||
}).optional(),
|
||||
region: z.string().trim().min(1).optional(),
|
||||
credentials: z.enum(["ambient", "static_files"]).default("ambient"),
|
||||
trusted_endpoint: z.boolean().default(false),
|
||||
allow_private_endpoint: z.boolean().default(false),
|
||||
allow_insecure_endpoint: z.boolean().default(false),
|
||||
max_bytes: positiveSafeInteger.default(10 * 1024 * 1024),
|
||||
max_objects: positiveSafeInteger.default(10_000),
|
||||
max_pages: positiveSafeInteger.default(100),
|
||||
page_size: positiveSafeInteger.max(1_000).default(1_000),
|
||||
}).strict().superRefine((source, context) => {
|
||||
if (source.endpoint_url === undefined) {
|
||||
if (source.trusted_endpoint || source.allow_private_endpoint || source.allow_insecure_endpoint) {
|
||||
context.addIssue({
|
||||
code: "custom", path: ["endpoint_url"],
|
||||
message: "S3 endpoint policy requires endpoint_url",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!source.trusted_endpoint) {
|
||||
context.addIssue({
|
||||
code: "custom", path: ["trusted_endpoint"],
|
||||
message: "custom S3 endpoints must be explicitly trusted",
|
||||
});
|
||||
}
|
||||
if (source.endpoint_url.startsWith("http:") && !source.allow_insecure_endpoint) {
|
||||
context.addIssue({
|
||||
code: "custom", path: ["allow_insecure_endpoint"],
|
||||
message: "HTTP S3 endpoints require an explicit insecure opt-in",
|
||||
});
|
||||
}
|
||||
});
|
||||
const evidenceSourceSchema = z.discriminatedUnion("type", [
|
||||
filesystemEvidenceSourceSchema,
|
||||
httpEvidenceSourceSchema,
|
||||
s3EvidenceSourceSchema,
|
||||
]);
|
||||
const evidencePolicySchema = z.object({
|
||||
max_chunk_chars: positiveSafeInteger.default(4_000),
|
||||
retain_published_generations: positiveSafeInteger.default(3),
|
||||
}).strict();
|
||||
const workspaceEvidenceSchema = z.object({
|
||||
source: evidenceSourceSchema,
|
||||
policy: evidencePolicySchema.default({
|
||||
max_chunk_chars: 4_000,
|
||||
retain_published_generations: 3,
|
||||
}),
|
||||
}).strict();
|
||||
|
||||
function unique<T>(values: readonly T[], context: z.RefinementCtx, path: PropertyKey[]) {
|
||||
if (new Set(values).size !== values.length) {
|
||||
context.addIssue({ code: "custom", path, message: "supported transports must not repeat" });
|
||||
@@ -235,6 +421,17 @@ function workspaceInvariants(workspace: any, context: z.RefinementCtx): void {
|
||||
}
|
||||
unique(workspace.llm_policy.allowed, context, ["llm_policy", "allowed"]);
|
||||
|
||||
if (workspace.evidence?.source.type === "filesystem") {
|
||||
const expected = `workspace-content/${workspace.workspace.id}/evidence`;
|
||||
if (workspace.evidence.source.uri !== expected) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["evidence", "source", "uri"],
|
||||
message: "filesystem evidence URI must be the canonical workspace Evidence root",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (workspace.semantic_index.vector_store.dimensions !== workspace.semantic_index.embedding.dimensions) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
@@ -303,6 +500,7 @@ const WorkspaceV2Schema = z.object({
|
||||
const WorkspaceV3Schema = z.object({
|
||||
dwh: dwhSchema,
|
||||
llm_policy: llmPolicySchema,
|
||||
evidence: workspaceEvidenceSchema.optional(),
|
||||
diagnostics: z.object({
|
||||
dwh_rest: dwhRestDiagnostic.optional(),
|
||||
}).strict().optional(),
|
||||
|
||||
@@ -16,96 +16,4 @@ export type WorkspaceErrorCode =
|
||||
| "git_auth_failed" | "git_non_fast_forward" | "git_push_rejected"
|
||||
| "connector_unavailable" | "semantic_index_incompatible";
|
||||
|
||||
export interface QdrantVectorStore {
|
||||
engine: "qdrant";
|
||||
collection: string;
|
||||
dimensions: 1024;
|
||||
distance: "cosine";
|
||||
}
|
||||
|
||||
export interface InternalEmbedding {
|
||||
provider: "ollama_internal";
|
||||
model: "qwen3-embedding:0.6b";
|
||||
dimensions: 1024;
|
||||
}
|
||||
|
||||
export interface WorkspaceV2 {
|
||||
workspace: {
|
||||
schema_version: 2;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
language: "en" | "it";
|
||||
};
|
||||
dwh: {
|
||||
engine: "postgres";
|
||||
database: string;
|
||||
schema: string;
|
||||
port?: number;
|
||||
timeout_ms?: number;
|
||||
supported_transports: ("postgres_direct" | "rest_api" | "ssh_tunnel")[];
|
||||
};
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
engine: "pgvector";
|
||||
database: string;
|
||||
schema: string;
|
||||
collection: string;
|
||||
dimensions: number;
|
||||
distance: "cosine" | "l2" | "inner_product";
|
||||
port?: number;
|
||||
timeout_ms?: number;
|
||||
supported_transports: ("pgvector_direct" | "rest_api" | "ssh_tunnel")[];
|
||||
};
|
||||
vector_writer?: Record<string, never>;
|
||||
embedding: {
|
||||
provider: "ollama_compatible" | "openai_compatible";
|
||||
model: string;
|
||||
dimensions: number;
|
||||
timeout_ms?: number;
|
||||
};
|
||||
};
|
||||
llm_policy: {
|
||||
default?: `${string}/${string}`;
|
||||
allowed: `${string}/${string}`[];
|
||||
};
|
||||
diagnostics?: {
|
||||
dwh_rest?: {
|
||||
method: "GET" | "POST";
|
||||
path: string;
|
||||
auth: "none" | "bearer" | "x-api-key";
|
||||
response: { database: string; schema: string };
|
||||
};
|
||||
vector_rest?: {
|
||||
metadata: {
|
||||
method: "GET" | "POST";
|
||||
path: string;
|
||||
auth: "none" | "bearer" | "x-api-key";
|
||||
response: { collection: string; dimensions: string; distance: string };
|
||||
};
|
||||
reversible_probe?: {
|
||||
method: "POST";
|
||||
path: string;
|
||||
auth: "bearer" | "x-api-key";
|
||||
response: { operation: string };
|
||||
};
|
||||
};
|
||||
embedding?: {
|
||||
method: "GET" | "POST";
|
||||
path: string;
|
||||
auth: "none" | "bearer" | "x-api-key";
|
||||
response: { model: string; dimensions: string };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkspaceV3 {
|
||||
workspace: WorkspaceV2["workspace"] & { schema_version: 3 };
|
||||
dwh: WorkspaceV2["dwh"];
|
||||
semantic_index: {
|
||||
vector_store: QdrantVectorStore;
|
||||
embedding: InternalEmbedding;
|
||||
};
|
||||
llm_policy: WorkspaceV2["llm_policy"];
|
||||
diagnostics?: Pick<NonNullable<WorkspaceV2["diagnostics"]>, "dwh_rest">;
|
||||
}
|
||||
export type { WorkspaceV2, WorkspaceV3 } from "./schema.js";
|
||||
|
||||
@@ -26,6 +26,7 @@ test("migrates the current local PSD descriptor without copying secret values",
|
||||
const result = migrateLegacyWorkspace(readFixture("local.yaml"), { id: "local", collection: "local" });
|
||||
|
||||
expect(result.workspace.workspace).toMatchObject({ id: "local", schema_version: 3, language: "it" });
|
||||
expect(result.workspace).not.toHaveProperty("evidence");
|
||||
expect(JSON.stringify(result)).not.toMatch(/password:|api_key:|\$\{THT_/i);
|
||||
});
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ test("migrates a schema v2 workspace to the internal qdrant schema v3 shape", ()
|
||||
|
||||
const migrated = migrateWorkspaceV2ToV3(legacy, "psd-clinical");
|
||||
|
||||
expect(migrated).not.toHaveProperty("evidence");
|
||||
expect(migrated).toMatchObject({
|
||||
workspace: { schema_version: 3, id: "psd-clinical", name: "Policlinico San Donato" },
|
||||
dwh: legacy.dwh,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
parseWorkspaceYaml,
|
||||
serializeWorkspaceYaml,
|
||||
validateCanonicalWorkspace,
|
||||
validateWorkspaceDescriptor,
|
||||
type WorkspaceDescriptor,
|
||||
} from "../src/workspaces/schema.js";
|
||||
|
||||
@@ -272,3 +273,316 @@ test("serializes canonical YAML that parses back to the same workspace", () => {
|
||||
expect(serializeWorkspaceYaml(parseWorkspaceYaml(serialized))).toBe(serialized);
|
||||
expect(parseWorkspaceYaml(serialized)).toEqual(workspace);
|
||||
});
|
||||
|
||||
|
||||
function validWorkspaceObject(): Record<string, any> {
|
||||
return parseWorkspaceYaml(validYaml) as Record<string, any>;
|
||||
}
|
||||
|
||||
function withEvidence(source: Record<string, unknown>, policy?: Record<string, unknown>): Record<string, any> {
|
||||
const workspace = structuredClone(validWorkspaceObject());
|
||||
workspace.evidence = policy === undefined ? { source } : { source, policy };
|
||||
return workspace;
|
||||
}
|
||||
|
||||
function expectSafeEvidenceError(workspace: unknown, path: RegExp, canary?: string): void {
|
||||
let message = "";
|
||||
try {
|
||||
validateWorkspaceDescriptor(workspace);
|
||||
} catch (error) {
|
||||
message = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
expect(message.replace(/\s+/g, " ")).toMatch(path);
|
||||
if (canary !== undefined) expect(message).not.toContain(canary);
|
||||
}
|
||||
|
||||
const explicitPolicy = { max_chunk_chars: 8_000, retain_published_generations: 5 };
|
||||
|
||||
const validEvidenceSources = [
|
||||
{
|
||||
name: "filesystem with explicit values",
|
||||
source: {
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
patterns: ["documents/**/*.pdf", "notes/*.md"],
|
||||
max_bytes: 12_000_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTP without authentication",
|
||||
source: {
|
||||
type: "http",
|
||||
uris: ["https://evidence.example/manifest.json", "http://evidence.example/files/list.txt"],
|
||||
authentication: "none",
|
||||
connect_timeout_ms: 2_000,
|
||||
read_timeout_ms: 20_000,
|
||||
max_bytes: 12_000_000,
|
||||
max_redirects: 2,
|
||||
allow_private_hosts: false,
|
||||
max_cache_bytes: 24_000_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTP signed URL manifest",
|
||||
source: {
|
||||
type: "http",
|
||||
uris: ["https://evidence.example/signed-urls.txt"],
|
||||
authentication: "signed_urls_file",
|
||||
connect_timeout_ms: 2_000,
|
||||
read_timeout_ms: 20_000,
|
||||
max_bytes: 12_000_000,
|
||||
max_redirects: 2,
|
||||
allow_private_hosts: true,
|
||||
max_cache_bytes: 24_000_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "S3 with ambient credentials",
|
||||
source: {
|
||||
type: "s3",
|
||||
uri: "s3://clinical-evidence/published/",
|
||||
region: "eu-west-1",
|
||||
credentials: "ambient",
|
||||
trusted_endpoint: false,
|
||||
allow_private_endpoint: false,
|
||||
allow_insecure_endpoint: false,
|
||||
max_bytes: 12_000_000,
|
||||
max_objects: 2_000,
|
||||
max_pages: 20,
|
||||
page_size: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "S3 with static-file credentials and a trusted endpoint",
|
||||
source: {
|
||||
type: "s3",
|
||||
uri: "s3://clinical-evidence/published/",
|
||||
endpoint_url: "https://objects.example",
|
||||
region: "eu-west-1",
|
||||
credentials: "static_files",
|
||||
trusted_endpoint: true,
|
||||
allow_private_endpoint: false,
|
||||
allow_insecure_endpoint: false,
|
||||
max_bytes: 12_000_000,
|
||||
max_objects: 2_000,
|
||||
max_pages: 20,
|
||||
page_size: 100,
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
test.each(validEvidenceSources)("accepts evidence source: $name", ({ source }) => {
|
||||
expect(validateWorkspaceDescriptor(withEvidence(source, explicitPolicy))).toMatchObject({
|
||||
evidence: { source, policy: explicitPolicy },
|
||||
});
|
||||
});
|
||||
|
||||
test("applies filesystem and policy defaults to the canonical descriptor", () => {
|
||||
const parsed = validateWorkspaceDescriptor(withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
}));
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
evidence: {
|
||||
source: {
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
patterns: ["**/*.md"],
|
||||
max_bytes: 10 * 1024 * 1024,
|
||||
},
|
||||
policy: { max_chunk_chars: 4_000, retain_published_generations: 3 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps evidence optional on schema v3", () => {
|
||||
expect(validateWorkspaceDescriptor(validWorkspaceObject())).not.toHaveProperty("evidence");
|
||||
});
|
||||
|
||||
test("serializes defaulted evidence canonically and parses it without loss", () => {
|
||||
const canonical = validateWorkspaceDescriptor(withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
}));
|
||||
if (canonical.workspace.schema_version !== 3) throw new Error("expected schema v3");
|
||||
|
||||
expect(parseWorkspaceYaml(serializeWorkspaceYaml(canonical))).toEqual(canonical);
|
||||
});
|
||||
|
||||
const invalidFilesystemPaths = [
|
||||
"/workspace-content/psd-clinical/evidence",
|
||||
"workspace-content/../psd-clinical/evidence",
|
||||
"workspace-content/./psd-clinical/evidence",
|
||||
"workspace-content//psd-clinical/evidence",
|
||||
"workspace-content/psd-clinical/evidence/..",
|
||||
"workspace-content\\psd-clinical\\evidence",
|
||||
"workspace-content/psd-clinical/evidence\u0000",
|
||||
"workspace-content/other-workspace/evidence",
|
||||
"workspace-content/psd-clinical",
|
||||
"workspace-content/psd-clinical/evidence/nested",
|
||||
];
|
||||
|
||||
test.each(invalidFilesystemPaths)("rejects unsafe or noncanonical filesystem URI %#", (uri) => {
|
||||
expectSafeEvidenceError(withEvidence({ type: "filesystem", uri }), /evidence.*source.*uri/i);
|
||||
});
|
||||
|
||||
const invalidPatterns = ["", "/absolute", "../escape", ".", "folder/./file", "folder//file", "folder/../file", "a\\b", "a\u0007b"];
|
||||
|
||||
test.each(invalidPatterns)("rejects unsafe evidence glob %#", (pattern) => {
|
||||
expectSafeEvidenceError(withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
patterns: [pattern],
|
||||
}), /evidence.*source.*patterns/i);
|
||||
});
|
||||
|
||||
test("rejects empty and duplicate filesystem patterns", () => {
|
||||
const source = { type: "filesystem", uri: "workspace-content/psd-clinical/evidence" };
|
||||
expectSafeEvidenceError(withEvidence({ ...source, patterns: [] }), /patterns/i);
|
||||
expectSafeEvidenceError(withEvidence({ ...source, patterns: ["**/*.pdf", "**/*.pdf"] }), /patterns/i);
|
||||
});
|
||||
|
||||
test.each(["ftp", "git", "unknown"])("rejects unsupported evidence discriminator %s", (type) => {
|
||||
expectSafeEvidenceError(withEvidence({ type, uri: "workspace-content/psd-clinical/evidence" }), /evidence.*source.*type/i);
|
||||
});
|
||||
|
||||
test("rejects unknown evidence keys", () => {
|
||||
expectSafeEvidenceError({ ...withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
}), evidence: {
|
||||
source: { type: "filesystem", uri: "workspace-content/psd-clinical/evidence", mystery: true },
|
||||
policy: explicitPolicy,
|
||||
mystery: true,
|
||||
} }, /unrecognized|mystery/i);
|
||||
});
|
||||
|
||||
const credentialFields = [
|
||||
"password", "api_key", "access_key", "secret_key", "session_token", "signed_url", "headers", "ca_contents",
|
||||
];
|
||||
|
||||
test.each(credentialFields)("rejects credential-shaped evidence field %s without leaking it", (field) => {
|
||||
const canary = `CANARY-${field}-DO-NOT-LEAK`;
|
||||
expectSafeEvidenceError(withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
[field]: canary,
|
||||
}), /evidence.*source/i, canary);
|
||||
});
|
||||
|
||||
const invalidHttpUris = [
|
||||
"https://user:CANARY-HTTP@example.com/manifest",
|
||||
"https://example.com/manifest?token=CANARY-HTTP",
|
||||
"https://example.com/manifest#CANARY-HTTP",
|
||||
"ftp://example.com/manifest/CANARY-HTTP",
|
||||
];
|
||||
|
||||
test.each(invalidHttpUris)("rejects unsafe HTTP descriptor URI %# without leaking it", (uri) => {
|
||||
expectSafeEvidenceError(withEvidence({ type: "http", uris: [uri] }), /evidence.*source.*uris.*0/i, "CANARY-HTTP");
|
||||
});
|
||||
|
||||
test("rejects empty and canonically duplicate HTTP manifests", () => {
|
||||
expectSafeEvidenceError(withEvidence({ type: "http", uris: [] }), /uris/i);
|
||||
expectSafeEvidenceError(withEvidence({
|
||||
type: "http",
|
||||
uris: ["https://EXAMPLE.com:443/manifest", "https://example.com/manifest"],
|
||||
}), /uris/i);
|
||||
});
|
||||
|
||||
const invalidHttpBounds = [
|
||||
["connect_timeout_ms", 0], ["read_timeout_ms", 0], ["max_bytes", 0],
|
||||
["max_redirects", -1], ["max_cache_bytes", 0], ["connect_timeout_ms", Number.MAX_SAFE_INTEGER + 1],
|
||||
] as const;
|
||||
|
||||
test.each(invalidHttpBounds)("rejects invalid HTTP bound %s=%s", (field, value) => {
|
||||
expectSafeEvidenceError(withEvidence({
|
||||
type: "http",
|
||||
uris: ["https://example.com/manifest"],
|
||||
[field]: value,
|
||||
}), new RegExp(field, "i"));
|
||||
});
|
||||
|
||||
const invalidS3Uris = [
|
||||
"https://bucket/prefix", "s3:///prefix", "s3://user:CANARY-S3@bucket/prefix",
|
||||
"s3://bucket/prefix?token=CANARY-S3", "s3://bucket/prefix#CANARY-S3",
|
||||
];
|
||||
|
||||
test.each(invalidS3Uris)("rejects invalid S3 URI %# without leaking it", (uri) => {
|
||||
expectSafeEvidenceError(withEvidence({ type: "s3", uri }), /evidence.*source.*uri/i, "CANARY-S3");
|
||||
});
|
||||
|
||||
const invalidS3Endpoints = [
|
||||
{ endpoint_url: "ftp://objects.example", trusted_endpoint: true },
|
||||
{ endpoint_url: "https://user:CANARY-S3@objects.example", trusted_endpoint: true },
|
||||
{ endpoint_url: "https://objects.example/path", trusted_endpoint: true },
|
||||
{ endpoint_url: "https://objects.example?token=CANARY-S3", trusted_endpoint: true },
|
||||
{ endpoint_url: "https://objects.example#CANARY-S3", trusted_endpoint: true },
|
||||
{ endpoint_url: "https://objects.example", trusted_endpoint: false },
|
||||
{ endpoint_url: "http://objects.example", trusted_endpoint: true, allow_insecure_endpoint: false },
|
||||
{ trusted_endpoint: true },
|
||||
{ allow_private_endpoint: true },
|
||||
{ allow_insecure_endpoint: true },
|
||||
];
|
||||
|
||||
test.each(invalidS3Endpoints)("rejects unsafe or inconsistent S3 endpoint %#", (endpoint) => {
|
||||
expectSafeEvidenceError(withEvidence({ type: "s3", uri: "s3://bucket/prefix", ...endpoint }), /evidence.*source/i, "CANARY-S3");
|
||||
});
|
||||
|
||||
const invalidS3Bounds = [
|
||||
["max_bytes", 0], ["max_objects", 0], ["max_pages", 0], ["page_size", 0],
|
||||
["max_objects", Number.MAX_SAFE_INTEGER + 1],
|
||||
] as const;
|
||||
|
||||
test.each(invalidS3Bounds)("rejects invalid S3 bound %s=%s", (field, value) => {
|
||||
expectSafeEvidenceError(withEvidence({
|
||||
type: "s3", uri: "s3://bucket/prefix", [field]: value,
|
||||
}), new RegExp(field, "i"));
|
||||
});
|
||||
|
||||
test.each([
|
||||
["max_chunk_chars", 0],
|
||||
["max_chunk_chars", Number.MAX_SAFE_INTEGER + 1],
|
||||
["retain_published_generations", 0],
|
||||
["retain_published_generations", Number.MAX_SAFE_INTEGER + 1],
|
||||
] as const)("rejects invalid evidence policy bound %s=%s", (field, value) => {
|
||||
expectSafeEvidenceError(withEvidence({
|
||||
type: "filesystem", uri: "workspace-content/psd-clinical/evidence",
|
||||
}, { ...explicitPolicy, [field]: value }), new RegExp(field, "i"));
|
||||
});
|
||||
|
||||
test("rejects evidence on strict schema v1 and v2 descriptors", () => {
|
||||
for (const schemaVersion of [1, 2]) {
|
||||
const yaml = validYaml
|
||||
.replace("schema_version: 3", `schema_version: ${schemaVersion}`)
|
||||
.replace(`semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024`, `semantic_index:
|
||||
vector_store:
|
||||
engine: pgvector
|
||||
database: postgres
|
||||
schema: vectors
|
||||
collection: documents
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
supported_transports:
|
||||
- pgvector_direct
|
||||
embedding:
|
||||
provider: ollama_compatible
|
||||
model: evidence-test
|
||||
dimensions: 1024`)
|
||||
+ `evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
uri: workspace-content/psd-clinical/evidence
|
||||
`;
|
||||
expect(() => parseWorkspaceYaml(yaml)).toThrow(/evidence|unrecognized/i);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user