@withtruing/companion 0.14.2 → 0.14.4

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.
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalJson } from './identity.js';
3
- import { sanitizeBuildMetadata } from './sanitize.js';
3
+ import { inspectFragmentSlice, sanitizeBuildMetadata } from './sanitize.js';
4
4
  import { WHOLE_READ_PAGE_PROTOCOL, } from './types.js';
5
5
  export const MAX_METADATA_PAGE_BYTES = 512 * 1024;
6
6
  export const MAX_TRANSPORT_RECORD_BYTES = 16 * 1024;
@@ -267,7 +267,7 @@ function assertTransportRecord(record) {
267
267
  catch {
268
268
  return fail('collection_invalid');
269
269
  }
270
- if (!sanitizeBuildMetadata(decoded).ok)
270
+ if (inspectFragmentSlice(decoded) !== null)
271
271
  fail('unsafe_metadata');
272
272
  }
273
273
  else
@@ -116,6 +116,73 @@ function inspectString(value) {
116
116
  return 'secret_value_detected';
117
117
  return containsCodeQuote(value) ? 'code_quote_detected' : null;
118
118
  }
119
+ const SIMPLE_ESCAPES = {
120
+ '"': '"', '\\': '\\', '/': '/', n: '\n', t: '\t', r: '\r', b: '\b', f: '\f',
121
+ };
122
+ function unescapeJsonText(raw) {
123
+ let result = '';
124
+ for (let index = 0; index < raw.length; index += 1) {
125
+ const character = raw[index];
126
+ if (character !== '\\') {
127
+ result += character;
128
+ continue;
129
+ }
130
+ const next = raw[index + 1];
131
+ if (next === undefined)
132
+ break; // escape cut at the edge: drop it
133
+ if (next === 'u') {
134
+ const hex = raw.slice(index + 2, index + 6);
135
+ if (!/^[0-9a-fA-F]{4}$/.test(hex))
136
+ break; // incomplete \uXXXX at the edge: drop it
137
+ result += String.fromCharCode(Number.parseInt(hex, 16));
138
+ index += 5;
139
+ }
140
+ else {
141
+ result += SIMPLE_ESCAPES[next] ?? next;
142
+ index += 1;
143
+ }
144
+ }
145
+ return result;
146
+ }
147
+ /**
148
+ * Splits a slice of serialized JSON on its unescaped double quotes and returns the unescaped text of
149
+ * every segment: complete strings, a partial string at the start (the slice began inside a string)
150
+ * and a partial string at the end (the slice ended inside one). Segments between strings are JSON
151
+ * structure; they are short and are returned too, which is harmless for the code-quote heuristic.
152
+ */
153
+ export function extractSliceStrings(text) {
154
+ const segments = [];
155
+ let current = '';
156
+ for (let index = 0; index < text.length; index += 1) {
157
+ const character = text[index];
158
+ if (character === '\\') {
159
+ current += character + (text[index + 1] ?? '');
160
+ index += 1;
161
+ }
162
+ else if (character === '"') {
163
+ segments.push(current);
164
+ current = '';
165
+ }
166
+ else {
167
+ current += character;
168
+ }
169
+ }
170
+ segments.push(current);
171
+ return segments.map(unescapeJsonText);
172
+ }
173
+ /**
174
+ * Checks one raw slice of a record's serialized JSON (a transport fragment). Secrets and absolute
175
+ * host paths are checked on the whole slice. The code-quote heuristic runs on the string values in
176
+ * the slice, never on the JSON structure (raw JSON always looks like code). A fragment edge can only
177
+ * shorten a string, so the full check on the reassembled record remains the backstop.
178
+ */
179
+ export function inspectFragmentSlice(text) {
180
+ if (containsHostPath(text))
181
+ return 'cleanup_path_forbidden';
182
+ if (validateNoSecretValues(text).ok === false)
183
+ return 'secret_value_detected';
184
+ return extractSliceStrings(text).some(containsCodeQuote) ? 'code_quote_detected' : null;
185
+ }
119
186
  function cloneMetadata(value, allowCleanupPath, field, ancestors = new Set(), depth = 0) {
120
187
  if (depth > 64)
121
188
  return { ok: false, code: 'metadata_schema_invalid' };
@@ -174,7 +174,10 @@ export function createCompanionAttemptSourceManager(dependencies = {}) {
174
174
  const reportCleanup = dependencies.reportCleanup ?? (async () => false);
175
175
  const scheduleInterval = dependencies.setInterval ?? setInterval;
176
176
  const cancelInterval = dependencies.clearInterval ?? clearInterval;
177
- const buildsEnabled = dependencies.buildsEnabled ?? (() => process.env.WHOLE_READ_BUILDS === '1');
177
+ // On by default: Truing's server decides whether build 3 runs (its own WHOLE_READ_BUILDS switch), and a
178
+ // released connector must carry it without the user setting anything (TRUING.md §9). WHOLE_READ_BUILDS=0
179
+ // on the user's computer is an emergency off switch.
180
+ const buildsEnabled = dependencies.buildsEnabled ?? (() => process.env.WHOLE_READ_BUILDS !== '0');
178
181
  const readers = new Map();
179
182
  const activeLocations = new Set();
180
183
  const cleanupJobs = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withtruing/companion",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "The Truing companion: reads your code and runs AI on your own computer, for Truing.",
5
5
  "type": "module",
6
6
  "license": "MIT",