@@ -2,12 +2,23 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
22import { homedir } from "node:os" ;
33import { dirname , join , resolve } from "node:path" ;
44import { createHash } from "node:crypto" ;
5+ import { type } from "arktype" ;
56import { getLogger } from "@intx/log" ;
67import type { MCPServerConfig } from "../config/settings.js" ;
78import { LOG_NAMESPACE_ROOT , SETTINGS_DIR_NAME } from "../branding.js" ;
89
910const logger = getLogger ( [ LOG_NAMESPACE_ROOT , "trust" ] ) ;
1011
12+ // Array fields are typed "unknown[]" rather than "string[]" because, unlike
13+ // path-trust.ts's strict schema, a mixed-type array here must keep its valid
14+ // string entries instead of invalidating the whole record — filtering happens
15+ // after arktype confirms the field is at least an array.
16+ const ProjectTrustRecordSchema = type ( {
17+ "trustedPluginPaths?" : "unknown[]" ,
18+ "trustedMcpFingerprints?" : "unknown[]" ,
19+ "repo?" : "string" ,
20+ } ) ;
21+
1122/** Where a plugin was discovered from. */
1223export type PluginOrigin = "repo" | "user" | "project" | "path" ;
1324
@@ -29,21 +40,15 @@ const emptyStore = (): ProjectTrustStore => ({
2940} ) ;
3041
3142/**
32- * Coerce a trust-store array field: missing → [], mixed types keep only strings,
33- * non-array → invalid (null). Hand-edited partial files must not wipe consent.
43+ * Extract a trust-store array field already confirmed to be an array (or
44+ * absent) by ProjectTrustRecordSchema: missing → [], mixed types keep only
45+ * strings. Hand-edited partial files must not wipe consent.
3446 */
35- function coerceStringArrayField (
36- value : unknown ,
37- field : string ,
38- path : string ,
39- ) : string [ ] | null {
47+ function extractStringArrayField ( value : unknown [ ] | undefined , field : string , path : string ) : string [ ] {
4048 if ( value === undefined ) {
4149 logger . warn `project trust store missing ${ field } at ${ path } ; defaulting to []` ;
4250 return [ ] ;
4351 }
44- if ( ! Array . isArray ( value ) ) {
45- return null ;
46- }
4752 const strings : string [ ] = [ ] ;
4853 let dropped = 0 ;
4954 for ( const entry of value ) {
@@ -99,39 +104,28 @@ export async function readProjectTrustStore(
99104 logger . warn `project trust store is not valid JSON at ${ path } : ${ String ( err ) } ` ;
100105 return { state : "invalid" , store : emptyStore ( ) } ;
101106 }
102- // Non-object JSON (arrays, null, primitives) cannot be a trust record.
103- if ( parsed === null || typeof parsed !== "object" || Array . isArray ( parsed ) ) {
104- logger . warn `project trust store has an invalid shape at ${ path } : expected object ` ;
107+ const validated = ProjectTrustRecordSchema ( parsed ) ;
108+ if ( validated instanceof type . errors ) {
109+ logger . warn `project trust store has an invalid shape at ${ path } : ${ validated . summary } ` ;
105110 return { state : "invalid" , store : emptyStore ( ) } ;
106111 }
107- const record = parsed as Record < string , unknown > ;
108112 // Coerce array fields instead of hard-rejecting: a hand-edited partial file
109113 // (only one list present) or a mixed-type array must keep valid string grants.
110- const trustedPluginPaths = coerceStringArrayField (
111- record . trustedPluginPaths ,
114+ const trustedPluginPaths = extractStringArrayField (
115+ validated . trustedPluginPaths ,
112116 "trustedPluginPaths" ,
113117 path ,
114118 ) ;
115- const trustedMcpFingerprints = coerceStringArrayField (
116- record . trustedMcpFingerprints ,
119+ const trustedMcpFingerprints = extractStringArrayField (
120+ validated . trustedMcpFingerprints ,
117121 "trustedMcpFingerprints" ,
118122 path ,
119123 ) ;
120- if ( trustedPluginPaths === null || trustedMcpFingerprints === null ) {
121- logger . warn `project trust store has an invalid shape at ${ path } : array fields must be arrays when present` ;
122- return { state : "invalid" , store : emptyStore ( ) } ;
123- }
124124 // Guard against a stale/copied record keyed to a different repo path: the
125125 // file records the repo it was written for and must match this cwd.
126- if ( record . repo !== undefined ) {
127- if ( typeof record . repo !== "string" ) {
128- logger . warn `project trust store has an invalid shape at ${ path } : repo must be a string when present` ;
129- return { state : "invalid" , store : emptyStore ( ) } ;
130- }
131- if ( resolve ( record . repo ) !== resolve ( cwd ) ) {
132- logger . warn `project trust store repo mismatch at ${ path } : recorded ${ record . repo } , expected ${ resolve ( cwd ) } ` ;
133- return { state : "invalid" , store : emptyStore ( ) } ;
134- }
126+ if ( validated . repo !== undefined && resolve ( validated . repo ) !== resolve ( cwd ) ) {
127+ logger . warn `project trust store repo mismatch at ${ path } : recorded ${ validated . repo } , expected ${ resolve ( cwd ) } ` ;
128+ return { state : "invalid" , store : emptyStore ( ) } ;
135129 }
136130 return {
137131 state : "valid" ,
0 commit comments