bare-agent 0.46.0 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -254,9 +254,9 @@ const { answers, costUsd, raw } = await jev.classify('I was charged twice, pleas
254
254
  // raw → the full unmodified parsed Jev response (a request id / warnings beyond answers/usage/model, if any)
255
255
  ```
256
256
 
257
- Three question types, kept verbatim from Jev's own contract: `noul` (binary probability `0..1`), `choice` (pick one of `criteria`'s keys, plus untrusted `probabilities`/`confidence`), `score` (a position `0..N-1` on `criteria`'s legend). Jev's reply is **untrusted model output** — `classify()` schema-checks the discriminator field against the question that asked it (`ValidationError`, stamped `lib:'bare-agent'`, on a type mismatch or an out-of-range value) before returning it; `probabilities`/`confidence` pass through unvalidated.
257
+ Three question types, kept verbatim from Jev's own contract: `noul` (binary probability `0..1`), `choice` (pick one of `criteria`'s keys, plus untrusted `probabilities`/`confidence`), `score` (a position `0..N-1` on `criteria`'s legend). Jev's reply is **untrusted model output** — `classify()` schema-checks the discriminator field against the question that asked it (`ValidationError`, stamped `lib:'bare-agent'`, on a type mismatch or an out-of-range value) before returning it; `probabilities`/`confidence` pass through unvalidated. A question's `instructions` can be a string, or the documented TypeSafe structured forms — an array or a plain object with named parts (e.g. `question`/`inspect`/`focus`/`ignore`) — for giving a question multiple named parts; empty `{}`/`[]`/non-plain values are rejected as missing instructions.
258
258
 
259
- **Injection hardening is on by default.** `state` is untrusted and can carry an embedded attack (`"you are now…"`, `"ignore previous instructions"`); `classify()` prepends a defensive preamble to every question's `instructions` (copy-on-write, never mutates your `questions` object) so the classifier treats `state` as data, not commands. Opt out with `harden: false` on the constructor or per call.
259
+ **Injection hardening is on by default, and shape-aware.** `state` is untrusted and can carry an embedded attack (`"you are now…"`, `"ignore previous instructions"`); `classify()` wraps every question's `instructions` with a defensive preamble prefixed onto a string, prepended as element 0 of an array, or added under a reserved `__hardening__` key on an object — so the classifier treats `state` as data, not commands (copy-on-write, never mutates your `questions` object; a caller-supplied `__hardening__` key is rejected so it can't overwrite the preamble). Opt out with `harden: false` on the constructor or per call.
260
260
 
261
261
  **Calibrate a tier before you trust it.** `calibrateJev` (exported from `bare-agent`, mirrors `calibrate`/judge's harness) grades a Jev model against a frozen clear-case battery (`noul`/`choice`/`score` + a false-positive trap) **and** a multi-style injection battery, admitting only if the clear-case floor clears with zero reds **and** every injection style is resisted:
262
262
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.46.0",
3
+ "version": "0.46.1",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts",
@@ -21,7 +21,7 @@ export class JevProvider {
21
21
  * @param {number} [options.deadlineMs] - Total call-duration deadline (ms); 0 disables (default).
22
22
  * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [options.rates] - Per-1K-token USD rates for authoritative pricing (Jev: `{ in: 0.042/1000, out: 0 }`).
23
23
  * @param {boolean} [options.exposeErrorBody=false] - Include the raw error body on a ProviderError (default off).
24
- * @param {boolean} [options.harden=true] - Prepend a defensive preamble to each question's instructions, treating `state` as untrusted data and resisting embedded role/label-override attempts. Overridable per-call via `opts.harden`.
24
+ * @param {boolean} [options.harden=true] - Wrap each question's instructions with a defensive preamble (prefixed for a string, prepended as element 0 for an array, or added under a reserved key for an object), treating `state` as untrusted data and resisting embedded role/label-override attempts. Overridable per-call via `opts.harden`.
25
25
  */
26
26
  constructor(options?: {
27
27
  apiKey?: string | undefined;
@@ -54,7 +54,7 @@ export class JevProvider {
54
54
  /**
55
55
  * Classify `state` against one or more typed `questions`. See the class doc for the primitive tags.
56
56
  * @param {string|object|any[]} state - The shared input all questions judge (Jev's `state`).
57
- * @param {Record<string, {type: 'noul'|'choice'|'score', instructions: string, criteria?: any}>} questions - Keyed questions; each judged independently against `state`.
57
+ * @param {Record<string, {type: 'noul'|'choice'|'score', instructions: string|object|any[], criteria?: any}>} questions - Keyed questions; each judged independently against `state`.
58
58
  * @param {object} [opts]
59
59
  * @param {string} [opts.model] - Override the model for this call.
60
60
  * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [opts.rates] - Override rates for this call.
@@ -68,7 +68,7 @@ export class JevProvider {
68
68
  */
69
69
  classify(state: string | object | any[], questions: Record<string, {
70
70
  type: "noul" | "choice" | "score";
71
- instructions: string;
71
+ instructions: string | object | any[];
72
72
  criteria?: any;
73
73
  }>, opts?: {
74
74
  model?: string | undefined;
@@ -101,9 +101,13 @@ export class JevProvider {
101
101
  */
102
102
  _validateRequest(state: any, questions: any): void;
103
103
  /**
104
- * Build a hardened COPY of `questions` (new object, new nested question objects) with
105
- * {@link HARDENING_PREAMBLE} prepended to each `instructions` string. Never mutates the
106
- * caller's `questions` argument or its nested objects. @param {Record<string, any>} questions @returns {Record<string, any>}
104
+ * Build a hardened COPY of `questions` (new object, new nested question objects, new
105
+ * nested instructions containers) with {@link HARDENING_PREAMBLE} woven into each
106
+ * `instructions`, shape-aware so a structured form is wrapped rather than stringified:
107
+ * a string gets the preamble prefixed, an array gets it prepended as element 0, and an
108
+ * object gets it added under the reserved {@link HARDENING_KEY}. Never mutates the
109
+ * caller's `questions` argument or its nested objects/arrays.
110
+ * @param {Record<string, any>} questions @returns {Record<string, any>}
107
111
  */
108
112
  _hardenQuestions(questions: Record<string, any>): Record<string, any>;
109
113
  /**
@@ -36,6 +36,9 @@ const CLASSIFY_PATH = '/v1/systemone';
36
36
  const DEFAULT_MODEL = 'jev-latest';
37
37
  const QUESTION_TYPES = new Set(['noul', 'choice', 'score']);
38
38
  const JEV_USAGE_KEYS = ['input_tokens', 'output_tokens'];
39
+ // Reserved key hardening uses to carry the preamble on an object-shaped `instructions` — a
40
+ // caller-supplied key of the same name would silently overwrite the preamble on spread.
41
+ const HARDENING_KEY = '__hardening__';
39
42
 
40
43
  // Injection hardening (default on): prepended to each question's `instructions` before the
41
44
  // request is sent, so an attack embedded in `state` (untrusted) can't hijack the classifier's
@@ -73,7 +76,7 @@ class JevProvider {
73
76
  * @param {number} [options.deadlineMs] - Total call-duration deadline (ms); 0 disables (default).
74
77
  * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [options.rates] - Per-1K-token USD rates for authoritative pricing (Jev: `{ in: 0.042/1000, out: 0 }`).
75
78
  * @param {boolean} [options.exposeErrorBody=false] - Include the raw error body on a ProviderError (default off).
76
- * @param {boolean} [options.harden=true] - Prepend a defensive preamble to each question's instructions, treating `state` as untrusted data and resisting embedded role/label-override attempts. Overridable per-call via `opts.harden`.
79
+ * @param {boolean} [options.harden=true] - Wrap each question's instructions with a defensive preamble (prefixed for a string, prepended as element 0 for an array, or added under a reserved key for an object), treating `state` as untrusted data and resisting embedded role/label-override attempts. Overridable per-call via `opts.harden`.
77
80
  */
78
81
  constructor(options = {}) {
79
82
  this.apiKey = options.apiKey;
@@ -89,7 +92,7 @@ class JevProvider {
89
92
  /**
90
93
  * Classify `state` against one or more typed `questions`. See the class doc for the primitive tags.
91
94
  * @param {string|object|any[]} state - The shared input all questions judge (Jev's `state`).
92
- * @param {Record<string, {type: 'noul'|'choice'|'score', instructions: string, criteria?: any}>} questions - Keyed questions; each judged independently against `state`.
95
+ * @param {Record<string, {type: 'noul'|'choice'|'score', instructions: string|object|any[], criteria?: any}>} questions - Keyed questions; each judged independently against `state`.
93
96
  * @param {object} [opts]
94
97
  * @param {string} [opts.model] - Override the model for this call.
95
98
  * @param {{in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}} [opts.rates] - Override rates for this call.
@@ -139,7 +142,14 @@ class JevProvider {
139
142
  const q = questions[id];
140
143
  if (!isPlainObject(q)) throw invalid(`question "${id}" must be an object`);
141
144
  if (!QUESTION_TYPES.has(q.type)) throw invalid(`question "${id}" has invalid type`, { type: q.type });
142
- if (typeof q.instructions !== 'string' || !q.instructions) throw invalid(`question "${id}" missing instructions`);
145
+ const ins = q.instructions;
146
+ const insValid = (typeof ins === 'string' && ins.length > 0) ||
147
+ (Array.isArray(ins) && ins.length > 0) ||
148
+ (isPlainObject(ins) && Object.keys(ins).length > 0);
149
+ if (!insValid) throw invalid(`question "${id}" missing instructions`);
150
+ if (isPlainObject(q.instructions) && Object.prototype.hasOwnProperty.call(q.instructions, HARDENING_KEY)) {
151
+ throw invalid(`question "${id}" may not use reserved instructions key`, { key: HARDENING_KEY });
152
+ }
143
153
  if (q.type === 'choice') {
144
154
  if (!isPlainObject(q.criteria) || Object.keys(q.criteria).length < 2) {
145
155
  throw invalid(`choice "${id}" needs a criteria object of >=2 {key: description}`);
@@ -156,16 +166,29 @@ class JevProvider {
156
166
  }
157
167
 
158
168
  /**
159
- * Build a hardened COPY of `questions` (new object, new nested question objects) with
160
- * {@link HARDENING_PREAMBLE} prepended to each `instructions` string. Never mutates the
161
- * caller's `questions` argument or its nested objects. @param {Record<string, any>} questions @returns {Record<string, any>}
169
+ * Build a hardened COPY of `questions` (new object, new nested question objects, new
170
+ * nested instructions containers) with {@link HARDENING_PREAMBLE} woven into each
171
+ * `instructions`, shape-aware so a structured form is wrapped rather than stringified:
172
+ * a string gets the preamble prefixed, an array gets it prepended as element 0, and an
173
+ * object gets it added under the reserved {@link HARDENING_KEY}. Never mutates the
174
+ * caller's `questions` argument or its nested objects/arrays.
175
+ * @param {Record<string, any>} questions @returns {Record<string, any>}
162
176
  */
163
177
  _hardenQuestions(questions) {
164
178
  /** @type {Record<string, any>} */
165
179
  const hardened = {};
166
180
  for (const id of Object.keys(questions)) {
167
181
  const q = questions[id];
168
- hardened[id] = { ...q, instructions: HARDENING_PREAMBLE + q.instructions };
182
+ const ins = q.instructions;
183
+ let hardenedIns;
184
+ if (typeof ins === 'string') {
185
+ hardenedIns = HARDENING_PREAMBLE + ins;
186
+ } else if (Array.isArray(ins)) {
187
+ hardenedIns = [HARDENING_PREAMBLE, ...ins];
188
+ } else {
189
+ hardenedIns = { [HARDENING_KEY]: HARDENING_PREAMBLE, ...ins };
190
+ }
191
+ hardened[id] = { ...q, instructions: hardenedIns };
169
192
  }
170
193
  return hardened;
171
194
  }