Hmbown/CodeWhale · error · Error
Duplicate .
Error message
Duplicate ${field}. What it means
The `strings` validator in evidence.ts parses an unknown value into a bounded, non-empty string array and rejects arrays containing repeated elements. This library treats duplicate entries in evidence fields (stages, surfaces, runIds, grant lists, etc.) as malformed policy/evidence input rather than silently de-duplicating, because duplicates in authorization data can change semantics. The thrown message includes the field name, e.g. `Duplicate runIds.`
Solutions
- De-duplicate the array before validation, e.g. `[...new Set(arr)]`
- Check the field name in the message and remove the repeated entry from the policy/bundle JSON
- If the duplicates come from merging configs, merge with a Set/union instead of concatenation
Example fix
// before
{"stages":["plan","plan","act"]}
// after
{"stages":["plan","act"]} Defensive patterns
Strategy: validation
Validate before calling
function assertNoDuplicates(arr, name) { if (new Set(arr).size !== arr.length) throw new Error(`Duplicate ${name}.`); } Type guard
const hasNoDuplicates = (a: unknown[]): a is string[] => new Set(a).size === a.length;
Try / catch
try { validatePolicy(input); } catch (e) { if (/^Duplicate /.test(e.message)) console.error('De-duplicate field named in message'); throw e; } Prevention
- Build lists with Set semantics when merging configs
- Run validatePolicy in CI on every policy file
- Store policies as canonical de-duplicated JSON
When it happens
Trigger: Calling validatePolicy or validateBundle with a policy whose source/grant arrays (stages, surfaces, runIds, sandboxIds, isolationGroups, targetIds, actions, effects) contain the same string twice; also validateObservation via sources/g when building arrays with accidental repeats.
Common situations: Hand-authored policy JSON where a run ID is copy-pasted twice; generated policies that append to an existing list without dedup; merging two configs where both contributed the same sandboxId.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Invalid Engine pet metadata fields.
- Invalid grant: object required.
- Invalid policy version or limits.
- Unknown field: .
- Codewhale terminal receipt contained a non-scalar field
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/8ab1dc9ba0395aa0.
Report an issue: GitHub.
Appendix: source
Thrown at pet/src/core/evidence.ts:75
unknownEffects: number; authority: { permitted: number; denied: number; unknown: number };
provenance: string;
}
const enumValue = <T extends string>(v:unknown, values: readonly T[], field:string): T => {
if(typeof v!=='string'||!values.includes(v as T))throw new Error(`Invalid ${field}.`);return v as T;
};
const object = (v:unknown, field:string): Record<string,unknown> => {
if(!v||typeof v!=='object'||Array.isArray(v))throw new Error(`Invalid ${field}: object required.`);return v as Record<string,unknown>;
};
const text = (v:unknown, field:string, max=256):string => {
if(typeof v!=='string'||!v.length||v.length>max||/[\u0000-\u001f\u007f]/.test(v))throw new Error(`Invalid ${field}: nonempty bounded text required.`);return v;
};
const num = (v:unknown,field:string,min=0):number => {
if(typeof v!=='number'||!Number.isFinite(v)||v<min||Math.abs(v)>Number.MAX_SAFE_INTEGER)throw new Error(`Invalid ${field}.`);return v;
};
const optionalText = (o:Record<string,unknown>,key:string):string|undefined => o[key]===undefined?undefined:text(o[key],key);
const strings = (v:unknown,field:string,allowEmpty=false):string[]=>{
if(!Array.isArray(v)||(!allowEmpty&&!v.length)||v.length>256)throw new Error(`Invalid ${field}.`);
const out=v.map(x=>text(x,field));if(new Set(out).size!==out.length)throw new Error(`Duplicate ${field}.`);return out;
};
const boolean = (v:unknown,field:string):boolean=>{if(typeof v!=='boolean')throw new Error(`Invalid ${field}.`);return v;};
const optionalNumber=(o:Record<string,unknown>,key:string)=>o[key]===undefined?undefined:num(o[key],key);
function onlyKeys(o:Record<string,unknown>,keys:string[],label:string):void{
for(const k of Object.keys(o))if(!keys.includes(k))throw new Error(`Unknown ${label} field: ${k}.`);
}
/** All unrecognized fields are rejected, never secretly retained in metadata-only evidence. */
export function validateObservation(input:unknown):Observation {
const o=object(input,'observation');
onlyKeys(o,['version','id','runId','operationId','sourceId','epoch','sequence','time','receivedAt','subject','stage','surface','action','effect','status','target','actionDigest','authority','correlation','facts'],'observation');
if(o.version!==1)throw new Error('Unsupported observation version.');
const t=object(o.time,'time'),s=object(o.subject,'subject');
onlyKeys(t,['wallMs','clockId','monotonicMs','taskMs','uncertaintyMs'],'time');onlyKeys(s,['agentId','sandboxId','isolationGroup'],'subject');
const seq=num(o.sequence,'sequence',1);if(!Number.isSafeInteger(seq))throw new Error('sequence must be an integer.');
const facts=object(o.facts??{},'facts'),safeFacts:Observation['facts']={};
if(Object.keys(facts).length>48)throw new Error('Too many observation facts.');
for(const [k,v] of Object.entries(facts)){
text(k,'fact key',80);if(['__proto__','prototype','constructor'].includes(k))throw new Error('Unsafe fact key.');View on GitHub (pinned to 433685b202)