@wrongstack/core 0.299.0 → 0.300.0

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.
Files changed (50) hide show
  1. package/dist/coordination/director.d.ts +8 -0
  2. package/dist/coordination/fleet-manager.d.ts +48 -3
  3. package/dist/coordination/ifleet-manager.d.ts +2 -0
  4. package/dist/coordination/index.js +120 -20
  5. package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
  6. package/dist/core/fallback-model.d.ts +48 -0
  7. package/dist/core/index.d.ts +3 -2
  8. package/dist/core/index.js +226 -26
  9. package/dist/core/instruction-template.d.ts +80 -0
  10. package/dist/core/system-prompt-blocks.d.ts +10 -1
  11. package/dist/core/system-prompt-builder.d.ts +35 -1
  12. package/dist/defaults/index.js +238 -99
  13. package/dist/execution/autonomy-brain.d.ts +7 -0
  14. package/dist/execution/council-brain.d.ts +11 -0
  15. package/dist/execution/council-orchestrator.d.ts +23 -4
  16. package/dist/execution/council-prompts.d.ts +12 -1
  17. package/dist/execution/index.js +355 -138
  18. package/dist/fleet-notifier.d.ts +9 -2
  19. package/dist/hooks/index.js +8 -4
  20. package/dist/hq/index.js +18 -4
  21. package/dist/hq/protocol/fleet.d.ts +20 -0
  22. package/dist/hq/protocol.js +10 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1512 -707
  25. package/dist/kernel/events/brain-events.d.ts +9 -0
  26. package/dist/kernel/events/provider-events.d.ts +42 -1
  27. package/dist/models/index.js +1 -1
  28. package/dist/plugin/api.d.ts +6 -0
  29. package/dist/plugin/config.d.ts +55 -0
  30. package/dist/plugin/index.d.ts +1 -1
  31. package/dist/plugin/index.js +134 -21
  32. package/dist/security/index.d.ts +1 -1
  33. package/dist/security/index.js +157 -42
  34. package/dist/security/permission-helpers.d.ts +23 -6
  35. package/dist/security/permission-policy.d.ts +16 -0
  36. package/dist/security/totp.d.ts +14 -0
  37. package/dist/storage/director-state.d.ts +7 -0
  38. package/dist/storage/index.js +33 -8
  39. package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
  40. package/dist/tools/index.js +388 -102
  41. package/dist/types/council.d.ts +11 -0
  42. package/dist/types/index.d.ts +1 -1
  43. package/dist/types/multi-agent.d.ts +10 -0
  44. package/dist/types/one-shot-llm.d.ts +9 -0
  45. package/dist/types/plugin.d.ts +28 -0
  46. package/dist/worktree/index.js +4 -4
  47. package/instructions/system-lite.md +81 -3
  48. package/instructions/system-pro.md +275 -90
  49. package/instructions/system.md +228 -81
  50. package/package.json +3 -3
@@ -712,22 +712,22 @@ var UNSAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set(["__proto__", "prototype", "
712
712
  function isRecord(value) {
713
713
  return typeof value === "object" && value !== null && !Array.isArray(value);
714
714
  }
715
- function error(diagnostics, code, path6, message) {
716
- diagnostics.push({ severity: "error", code, path: path6, message });
715
+ function error(diagnostics, code, path9, message) {
716
+ diagnostics.push({ severity: "error", code, path: path9, message });
717
717
  }
718
- function validateStringList(value, path6, diagnostics, limits) {
718
+ function validateStringList(value, path9, diagnostics, limits) {
719
719
  if (!Array.isArray(value)) {
720
- error(diagnostics, limits.listErrorCode, path6, `must be an array of strings`);
720
+ error(diagnostics, limits.listErrorCode, path9, `must be an array of strings`);
721
721
  return void 0;
722
722
  }
723
723
  if (value.length > limits.maxItems) {
724
- error(diagnostics, limits.tooManyCode, path6, `must contain at most ${limits.maxItems} ${limits.itemLabel}s`);
724
+ error(diagnostics, limits.tooManyCode, path9, `must contain at most ${limits.maxItems} ${limits.itemLabel}s`);
725
725
  return void 0;
726
726
  }
727
727
  const seen = /* @__PURE__ */ new Set();
728
728
  const out = [];
729
729
  for (const [index, entry] of value.entries()) {
730
- const itemPath = `${path6}[${index}]`;
730
+ const itemPath = `${path9}[${index}]`;
731
731
  if (typeof entry !== "string" || entry.length === 0 || entry.length > limits.maxChars) {
732
732
  error(
733
733
  diagnostics,
@@ -1467,6 +1467,7 @@ function numericValue(value) {
1467
1467
 
1468
1468
  // src/security/permission-policy.ts
1469
1469
  import * as fs from "node:fs/promises";
1470
+ import * as path6 from "node:path";
1470
1471
 
1471
1472
  // src/utils/atomic-write.ts
1472
1473
  import {
@@ -1767,19 +1768,19 @@ var UNSAFE_PROPERTY_NAMES2 = /* @__PURE__ */ new Set(["__proto__", "prototype",
1767
1768
  function isRecord2(value) {
1768
1769
  return typeof value === "object" && value !== null && !Array.isArray(value);
1769
1770
  }
1770
- function error2(diagnostics, code, path6, message) {
1771
- diagnostics.push({ severity: "error", code, path: path6, message });
1771
+ function error2(diagnostics, code, path9, message) {
1772
+ diagnostics.push({ severity: "error", code, path: path9, message });
1772
1773
  }
1773
- function validatePatterns(value, path6, diagnostics) {
1774
+ function validatePatterns(value, path9, diagnostics) {
1774
1775
  if (!Array.isArray(value)) {
1775
- error2(diagnostics, "invalid_pattern_list", path6, "must be an array of strings");
1776
+ error2(diagnostics, "invalid_pattern_list", path9, "must be an array of strings");
1776
1777
  return void 0;
1777
1778
  }
1778
1779
  if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
1779
1780
  error2(
1780
1781
  diagnostics,
1781
1782
  "too_many_patterns",
1782
- path6,
1783
+ path9,
1783
1784
  `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
1784
1785
  );
1785
1786
  return void 0;
@@ -1787,7 +1788,7 @@ function validatePatterns(value, path6, diagnostics) {
1787
1788
  const patterns = [];
1788
1789
  const seen = /* @__PURE__ */ new Set();
1789
1790
  for (const [index, pattern] of value.entries()) {
1790
- const itemPath = `${path6}[${index}]`;
1791
+ const itemPath = `${path9}[${index}]`;
1791
1792
  if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
1792
1793
  error2(
1793
1794
  diagnostics,
@@ -2201,6 +2202,19 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2201
2202
  }
2202
2203
  };
2203
2204
 
2205
+ // src/security/permission-helpers.ts
2206
+ import { realpathSync } from "node:fs";
2207
+ import * as path5 from "node:path";
2208
+
2209
+ // src/utils/wstack-paths.ts
2210
+ import * as os from "node:os";
2211
+ import * as path4 from "node:path";
2212
+ function wstackGlobalRoot() {
2213
+ const fromEnv = process.env["WRONGSTACK_HOME"];
2214
+ if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
2215
+ return path4.join(os.homedir(), ".wrongstack");
2216
+ }
2217
+
2204
2218
  // src/security/permission-helpers.ts
2205
2219
  function matchesTrust(patterns, subject) {
2206
2220
  return patterns.includes(subject) || matchAny(patterns, subject);
@@ -2272,9 +2286,51 @@ var SHELL_READ_VERBS = /* @__PURE__ */ new Set([
2272
2286
  function stripShellQuotes(value) {
2273
2287
  return value.replace(/^['"]|['"]$/g, "");
2274
2288
  }
2289
+ var AGENT_STATE_SENSITIVE_BASENAMES = /^(?:config\.json|config\.local\.json|trust\.json|auth\.json|\.key)$/i;
2290
+ function unescapeGlobSubject(value) {
2291
+ return value.replace(/\\([*?[\]])/g, "$1");
2292
+ }
2293
+ function normalizeForCompare(value) {
2294
+ const forward = unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "");
2295
+ return process.platform === "win32" ? forward.toLowerCase() : forward;
2296
+ }
2297
+ function realpathOfNearestExisting(p) {
2298
+ let probe = p;
2299
+ const tail = [];
2300
+ for (; ; ) {
2301
+ try {
2302
+ return tail.length === 0 ? realpathSync(probe) : path5.join(realpathSync(probe), ...tail);
2303
+ } catch {
2304
+ const parent = path5.dirname(probe);
2305
+ if (parent === probe) return p;
2306
+ tail.unshift(path5.basename(probe));
2307
+ probe = parent;
2308
+ }
2309
+ }
2310
+ }
2311
+ var agentStateRootRealCache = /* @__PURE__ */ new Map();
2312
+ function isInsideAgentStateRoot(absPath) {
2313
+ const lexicalRoot = normalizeForCompare(path5.resolve(wstackGlobalRoot()));
2314
+ if (!lexicalRoot) return false;
2315
+ const target = normalizeForCompare(absPath);
2316
+ if (target === lexicalRoot || target.startsWith(`${lexicalRoot}/`)) return true;
2317
+ let realRoot = agentStateRootRealCache.get(lexicalRoot);
2318
+ if (realRoot === void 0) {
2319
+ realRoot = normalizeForCompare(realpathOfNearestExisting(path5.resolve(wstackGlobalRoot())));
2320
+ agentStateRootRealCache.set(lexicalRoot, realRoot);
2321
+ }
2322
+ if (!realRoot || realRoot === lexicalRoot) return false;
2323
+ const realTarget = normalizeForCompare(realpathOfNearestExisting(absPath));
2324
+ return realTarget === realRoot || realTarget.startsWith(`${realRoot}/`);
2325
+ }
2326
+ function isProtectedAgentStatePath(absPath) {
2327
+ if (!isInsideAgentStateRoot(absPath)) return false;
2328
+ return AGENT_STATE_SENSITIVE_BASENAMES.test(path5.basename(normalizeForCompare(absPath)));
2329
+ }
2275
2330
  function pathLooksSensitive(rawPath) {
2276
2331
  const normalized = stripShellQuotes(rawPath).replace(/\\/g, "/");
2277
- return SENSITIVE_READ_PATHS.some((pattern) => pattern.test(normalized));
2332
+ if (SENSITIVE_READ_PATHS.some((pattern) => pattern.test(normalized))) return true;
2333
+ return isProtectedAgentStatePath(normalized);
2278
2334
  }
2279
2335
  function inputPathLooksSensitive(input) {
2280
2336
  if (!input || typeof input !== "object") return false;
@@ -2298,6 +2354,34 @@ function shellCommandReadsSensitivePath(command) {
2298
2354
  }
2299
2355
 
2300
2356
  // src/security/permission-policy.ts
2357
+ function fsWriteTargetPaths(input) {
2358
+ const out = [];
2359
+ if (!input || typeof input !== "object") return out;
2360
+ const obj = input;
2361
+ for (const key of [
2362
+ "path",
2363
+ "file_path",
2364
+ "file",
2365
+ "filePath",
2366
+ "files",
2367
+ "target",
2368
+ "targetPath",
2369
+ "out",
2370
+ "directory",
2371
+ "cwd",
2372
+ "template"
2373
+ ]) {
2374
+ const value = obj[key];
2375
+ if (typeof value === "string") {
2376
+ if (value.length > 0) out.push(value);
2377
+ } else if (Array.isArray(value)) {
2378
+ for (const item of value) {
2379
+ if (typeof item === "string" && item.length > 0) out.push(item);
2380
+ }
2381
+ }
2382
+ }
2383
+ return out;
2384
+ }
2301
2385
  var DefaultPermissionPolicy = class {
2302
2386
  policy = {};
2303
2387
  loaded = false;
@@ -2383,6 +2467,30 @@ var DefaultPermissionPolicy = class {
2383
2467
  getYoloDestructive() {
2384
2468
  return this.yoloDestructive;
2385
2469
  }
2470
+ /**
2471
+ * True when this call is an FS_WRITE whose target lands inside WrongStack's
2472
+ * own state root.
2473
+ *
2474
+ * FS_WRITE tools disagree on where the target path lives: write/edit use
2475
+ * `path`, replace/format/git `files` (string OR array), patch `directory`,
2476
+ * design `out`, scaffold writes under `cwd`/`template`. Checking only
2477
+ * `path`/`file_path` let a state-root write merely switch tools, so every
2478
+ * path-bearing key is inspected.
2479
+ *
2480
+ * Shared by the YOLO carve-out and the eval-cache guard so the two cannot
2481
+ * drift: the carve-out scans all path keys while the cache key fingerprints
2482
+ * only the subject key, so the cache must consult the same predicate or a
2483
+ * secondary state-root key can be replayed a cached `auto`.
2484
+ */
2485
+ hasAgentStateWriteTarget(tool, input, ctx) {
2486
+ if (!hasCapability(tool, ToolCapabilities.FS_WRITE)) return false;
2487
+ for (const targetPath of fsWriteTargetPaths(input)) {
2488
+ const base = ctx.workingDir ?? ctx.cwd;
2489
+ const resolved = base ? path6.resolve(base, targetPath) : path6.resolve(targetPath);
2490
+ if (isInsideAgentStateRoot(resolved)) return true;
2491
+ }
2492
+ return false;
2493
+ }
2386
2494
  /**
2387
2495
  * True when YOLO is on but this specific call is a destructive shell command
2388
2496
  * the user has not opted to auto-approve. Shared by `evaluate()` and the
@@ -2390,6 +2498,7 @@ var DefaultPermissionPolicy = class {
2390
2498
  */
2391
2499
  yoloBlockedAsDestructive(tool, input, ctx) {
2392
2500
  if (!this.yolo || this.yoloDestructive) return false;
2501
+ if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
2393
2502
  const isShellSurface = tool.name === "bash" || tool.name === "exec" || (tool.capabilities ?? []).includes("shell.arbitrary");
2394
2503
  if (!isShellSurface) return false;
2395
2504
  const command = getInputString(input, "command") ?? shellCommandLineFromInput(input);
@@ -2496,7 +2605,7 @@ var DefaultPermissionPolicy = class {
2496
2605
  const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
2497
2606
  const cacheKey = `${tool.name}::${subject ?? tool.name}`;
2498
2607
  const evalKey = `${cacheKey}::${permissionFingerprint(tool)}`;
2499
- if (tool.name !== "write") {
2608
+ if (tool.name !== "write" && !this.hasAgentStateWriteTarget(tool, input, ctx)) {
2500
2609
  const cached = this._evalCache.get(evalKey);
2501
2610
  if (cached !== void 0) return cached;
2502
2611
  }
@@ -2604,7 +2713,7 @@ var DefaultPermissionPolicy = class {
2604
2713
  return decision;
2605
2714
  }
2606
2715
  if (tool.name === "write" && subject) {
2607
- if (ctx.hasRead(subject)) {
2716
+ if (ctx.hasRead(subject) && !isInsideAgentStateRoot(subject)) {
2608
2717
  return {
2609
2718
  permission: "auto",
2610
2719
  source: "context",
@@ -2954,13 +3063,14 @@ var DefaultPermissionPolicy = class {
2954
3063
  }
2955
3064
  add("yolo", false, "auto", "yolo", "YOLO mode is not active");
2956
3065
  if (tool.name === "write" && subject) {
2957
- const hasRead = ctx.hasRead(subject);
3066
+ const isAgentState = isInsideAgentStateRoot(subject);
3067
+ const hasRead = ctx.hasRead(subject) && !isAgentState;
2958
3068
  add(
2959
3069
  "write smart bypass",
2960
3070
  hasRead,
2961
3071
  "auto",
2962
3072
  "context",
2963
- hasRead ? `file "${subject}" was already read in this session \u2014 auto-approving write` : `file "${subject}" was not read in this session \u2014 bypass does not apply`
3073
+ isAgentState ? `file "${subject}" is WrongStack's own state \u2014 bypass never applies, write always confirms` : hasRead ? `file "${subject}" was already read in this session \u2014 auto-approving write` : `file "${subject}" was not read in this session \u2014 bypass does not apply`
2964
3074
  );
2965
3075
  if (hasRead) {
2966
3076
  winnerIndex = steps.length - 1;
@@ -3042,7 +3152,7 @@ var DefaultPermissionPolicy = class {
3042
3152
  };
3043
3153
 
3044
3154
  // src/security/readonly-permission-policy.ts
3045
- import * as path4 from "node:path";
3155
+ import * as path7 from "node:path";
3046
3156
  var MUTATION_CAPABILITIES = /* @__PURE__ */ new Set([
3047
3157
  ToolCapabilities.FS_WRITE,
3048
3158
  ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
@@ -3066,9 +3176,9 @@ function isAllowedReportFile(input, projectRoot) {
3066
3176
  if (typeof inputPath !== "string") return false;
3067
3177
  if (!inputPath.endsWith(".md")) return false;
3068
3178
  try {
3069
- const resolved = path4.resolve(projectRoot, inputPath);
3070
- const tempDir = path4.resolve(projectRoot, ".temp_files");
3071
- if (!resolved.startsWith(tempDir + path4.sep) && resolved !== tempDir) return false;
3179
+ const resolved = path7.resolve(projectRoot, inputPath);
3180
+ const tempDir = path7.resolve(projectRoot, ".temp_files");
3181
+ if (!resolved.startsWith(tempDir + path7.sep) && resolved !== tempDir) return false;
3072
3182
  return true;
3073
3183
  } catch {
3074
3184
  return false;
@@ -3217,16 +3327,20 @@ function generateTotp(secretBase32, atMs = Date.now(), stepSeconds = DEFAULT_STE
3217
3327
  return hotp(secret, counter, digits);
3218
3328
  }
3219
3329
  function verifyTotp(code, secretBase32, atMs = Date.now(), window = DEFAULT_WINDOW, stepSeconds = DEFAULT_STEP_SECONDS, digits = DEFAULT_DIGITS) {
3220
- if (typeof code !== "string" || code.length !== digits) return false;
3330
+ return verifyTotpCounter(code, secretBase32, atMs, window, stepSeconds, digits) !== void 0;
3331
+ }
3332
+ function verifyTotpCounter(code, secretBase32, atMs = Date.now(), window = DEFAULT_WINDOW, stepSeconds = DEFAULT_STEP_SECONDS, digits = DEFAULT_DIGITS) {
3333
+ if (typeof code !== "string" || code.length !== digits) return void 0;
3221
3334
  const secret = base32Decode(secretBase32);
3222
3335
  const currentCounter = totpCounter(Math.floor(atMs / 1e3), stepSeconds);
3223
3336
  for (let offset = -window; offset <= window; offset++) {
3224
- const expected = hotp(secret, currentCounter + offset, digits);
3337
+ const counter = currentCounter + offset;
3338
+ const expected = hotp(secret, counter, digits);
3225
3339
  if (timingSafeEqual(Buffer.from(expected), Buffer.from(code))) {
3226
- return true;
3340
+ return counter;
3227
3341
  }
3228
3342
  }
3229
- return false;
3343
+ return void 0;
3230
3344
  }
3231
3345
  function buildOtpAuthUri(secretBase32, account, issuer = "WrongStack HQ", stepSeconds = DEFAULT_STEP_SECONDS, digits = DEFAULT_DIGITS) {
3232
3346
  const label = `${encodeURIComponent(issuer)}:${encodeURIComponent(account)}`;
@@ -3275,7 +3389,7 @@ function verifyRecoveryCode(code, storedHashes) {
3275
3389
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, scryptSync } from "node:crypto";
3276
3390
  import * as fs2 from "node:fs";
3277
3391
  import * as fsp from "node:fs/promises";
3278
- import * as path5 from "node:path";
3392
+ import * as path8 from "node:path";
3279
3393
 
3280
3394
  // src/utils/deep-merge.ts
3281
3395
  var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
@@ -3578,7 +3692,7 @@ var DefaultSecretVault = class {
3578
3692
  const oldVersion = this._keyVersion;
3579
3693
  const newKey = randomBytes2(KEY_BYTES);
3580
3694
  const newVersion = oldVersion + 1;
3581
- fs2.mkdirSync(path5.dirname(this.keyFile), { recursive: true });
3695
+ fs2.mkdirSync(path8.dirname(this.keyFile), { recursive: true });
3582
3696
  const passphrase = getVaultPassphrase();
3583
3697
  if (passphrase) {
3584
3698
  writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
@@ -3665,7 +3779,7 @@ var DefaultSecretVault = class {
3665
3779
  } catch (err) {
3666
3780
  if (err.code !== "ENOENT") throw err;
3667
3781
  }
3668
- fs2.mkdirSync(path5.dirname(this.keyFile), { recursive: true });
3782
+ fs2.mkdirSync(path8.dirname(this.keyFile), { recursive: true });
3669
3783
  const key = randomBytes2(KEY_BYTES);
3670
3784
  const passphrase = getVaultPassphrase();
3671
3785
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
@@ -3725,7 +3839,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
3725
3839
  }
3726
3840
  const merged = deepMerge(current, patch ?? {});
3727
3841
  const encrypted = encryptConfigSecrets(merged, vault);
3728
- await fsp.mkdir(path5.dirname(configPath), { recursive: true });
3842
+ await fsp.mkdir(path8.dirname(configPath), { recursive: true });
3729
3843
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
3730
3844
  await restrictFilePermissions2(configPath);
3731
3845
  await vault.flushHardening?.();
@@ -3947,33 +4061,33 @@ var RESERVED_BYPASS_FIELDS = /* @__PURE__ */ new Set([
3947
4061
  function isRecord3(value) {
3948
4062
  return typeof value === "object" && value !== null && !Array.isArray(value);
3949
4063
  }
3950
- function addString(issues, value, path6, required = true) {
4064
+ function addString(issues, value, path9, required = true) {
3951
4065
  if (typeof value === "string" && value.trim().length > 0) return true;
3952
4066
  if (!required && value === void 0) return false;
3953
- issues.push({ path: path6, message: "must be a non-empty string" });
4067
+ issues.push({ path: path9, message: "must be a non-empty string" });
3954
4068
  return false;
3955
4069
  }
3956
- function addEnum(issues, value, path6, values) {
4070
+ function addEnum(issues, value, path9, values) {
3957
4071
  if (typeof value === "string" && values.has(value)) return true;
3958
- issues.push({ path: path6, message: `must be one of: ${[...values].join(", ")}` });
4072
+ issues.push({ path: path9, message: `must be one of: ${[...values].join(", ")}` });
3959
4073
  return false;
3960
4074
  }
3961
- function validateAttributes(issues, value, path6) {
4075
+ function validateAttributes(issues, value, path9) {
3962
4076
  if (!isRecord3(value)) {
3963
- issues.push({ path: path6, message: "must be an object" });
4077
+ issues.push({ path: path9, message: "must be an object" });
3964
4078
  return false;
3965
4079
  }
3966
4080
  let valid = true;
3967
4081
  for (const [key, item] of Object.entries(value)) {
3968
4082
  if (RESERVED_BYPASS_FIELDS.has(key)) {
3969
4083
  issues.push({
3970
- path: `${path6}.${key}`,
4084
+ path: `${path9}.${key}`,
3971
4085
  message: "generic authorization bypass fields are forbidden"
3972
4086
  });
3973
4087
  valid = false;
3974
4088
  } else if (item !== null && !["string", "number", "boolean"].includes(typeof item)) {
3975
4089
  issues.push({
3976
- path: `${path6}.${key}`,
4090
+ path: `${path9}.${key}`,
3977
4091
  message: "must be a string, number, boolean, or null"
3978
4092
  });
3979
4093
  valid = false;
@@ -3981,16 +4095,16 @@ function validateAttributes(issues, value, path6) {
3981
4095
  }
3982
4096
  return valid;
3983
4097
  }
3984
- function validateScope(issues, value, path6) {
4098
+ function validateScope(issues, value, path9) {
3985
4099
  if (!isRecord3(value)) {
3986
- issues.push({ path: path6, message: "must be an object" });
4100
+ issues.push({ path: path9, message: "must be an object" });
3987
4101
  return false;
3988
4102
  }
3989
4103
  for (const field of ["projectId", "sessionId", "cwd"]) {
3990
- if (value[field] !== void 0) addString(issues, value[field], `${path6}.${field}`);
4104
+ if (value[field] !== void 0) addString(issues, value[field], `${path9}.${field}`);
3991
4105
  }
3992
4106
  if (value.constraints !== void 0)
3993
- validateAttributes(issues, value.constraints, `${path6}.constraints`);
4107
+ validateAttributes(issues, value.constraints, `${path9}.constraints`);
3994
4108
  return true;
3995
4109
  }
3996
4110
  function decodeTrustBoundaryRequest(input) {
@@ -4116,6 +4230,7 @@ export {
4116
4230
  validateDirectoryPolicy,
4117
4231
  validateTrustPolicy,
4118
4232
  verifyRecoveryCode,
4119
- verifyTotp
4233
+ verifyTotp,
4234
+ verifyTotpCounter
4120
4235
  };
4121
4236
  //# sourceMappingURL=index.js.map
@@ -1,9 +1,3 @@
1
- /**
2
- * Pure helpers for permission policy evaluation — trust-pattern matching,
3
- * tool fingerprinting, and sensitive-read detection.
4
- *
5
- * Extracted from permission-policy.ts. No class state, no I/O.
6
- */
7
1
  import type { Tool } from '../types/tool.js';
8
2
  /**
9
3
  * Match a computed subject against stored trust patterns.
@@ -88,6 +82,29 @@ export declare function alwaysAllowUnavailableReason(tool: Tool, input: unknown)
88
82
  */
89
83
  export declare function permissionFingerprint(tool: Tool): string;
90
84
  export declare function shellCommandLineFromInput(input: unknown): string | undefined;
85
+ /**
86
+ * True when `absPath` is the wstack global root (`~/.wrongstack`, or
87
+ * `WRONGSTACK_HOME`) or lives inside it.
88
+ *
89
+ * Callers use this to refuse *silent* access to WrongStack's own state. It is
90
+ * not a containment check — the file tools deliberately allow this root so the
91
+ * agent can manage its memory and sessions. What it gates is whether that
92
+ * access may happen without the user seeing it.
93
+ *
94
+ * The comparison is realpath-aware on both sides: `~/.wrongstack` may itself
95
+ * be a symlink, and the file tools canonicalize subjects to realpaths
96
+ * (`safeResolveReal`, WS-048), so a purely lexical check misses every state
97
+ * file under a symlinked root. The lexical pass stays first — it is free and
98
+ * correct for the default layout — and the realpath pass only runs when it
99
+ * fails, comparing the root (cached) against the subject's nearest existing
100
+ * ancestor.
101
+ */
102
+ export declare function isInsideAgentStateRoot(absPath: string): boolean;
103
+ /**
104
+ * True when writing `absPath` must not be silently auto-approved, because the
105
+ * file is part of WrongStack's own trusted state rather than project content.
106
+ */
107
+ export declare function isProtectedAgentStatePath(absPath: string): boolean;
91
108
  export declare function inputPathLooksSensitive(input: unknown): boolean;
92
109
  export declare function shellCommandReadsSensitivePath(command: string): boolean;
93
110
  //# sourceMappingURL=permission-helpers.d.ts.map
@@ -92,6 +92,22 @@ export declare class DefaultPermissionPolicy implements PermissionPolicy {
92
92
  setYoloDestructive(enabled: boolean): void;
93
93
  /** Whether YOLO currently extends to destructive shell commands. */
94
94
  getYoloDestructive(): boolean;
95
+ /**
96
+ * True when this call is an FS_WRITE whose target lands inside WrongStack's
97
+ * own state root.
98
+ *
99
+ * FS_WRITE tools disagree on where the target path lives: write/edit use
100
+ * `path`, replace/format/git `files` (string OR array), patch `directory`,
101
+ * design `out`, scaffold writes under `cwd`/`template`. Checking only
102
+ * `path`/`file_path` let a state-root write merely switch tools, so every
103
+ * path-bearing key is inspected.
104
+ *
105
+ * Shared by the YOLO carve-out and the eval-cache guard so the two cannot
106
+ * drift: the carve-out scans all path keys while the cache key fingerprints
107
+ * only the subject key, so the cache must consult the same predicate or a
108
+ * secondary state-root key can be replayed a cached `auto`.
109
+ */
110
+ private hasAgentStateWriteTarget;
95
111
  /**
96
112
  * True when YOLO is on but this specific call is a destructive shell command
97
113
  * the user has not opted to auto-approve. Shared by `evaluate()` and the
@@ -55,6 +55,20 @@ export declare function generateTotp(secretBase32: string, atMs?: number, stepSe
55
55
  * @returns `true` if the code matches any step within the window.
56
56
  */
57
57
  export declare function verifyTotp(code: string, secretBase32: string, atMs?: number, window?: number, stepSeconds?: number, digits?: number): boolean;
58
+ /**
59
+ * Like {@link verifyTotp}, but returns the time-step counter the code matched
60
+ * so the caller can enforce single use.
61
+ *
62
+ * RFC 6238 §5.2 requires that a code be accepted only once: the validation
63
+ * window is ±1 step by default, so a code observed in transit stays
64
+ * arithmetically valid for up to ~90 seconds. Recording the matched counter and
65
+ * refusing anything at or below it turns that window into one-shot. HQ's
66
+ * `/api/login/verify` does this against `mutableAuth.totpLastUsedCounter`.
67
+ *
68
+ * @returns the matched counter, or `undefined` when no step in the window
69
+ * matches.
70
+ */
71
+ export declare function verifyTotpCounter(code: string, secretBase32: string, atMs?: number, window?: number, stepSeconds?: number, digits?: number): number | undefined;
58
72
  /**
59
73
  * Build an `otpauth://` URI for QR-code provisioning (RFC 6238 / Google Auth).
60
74
  *
@@ -127,6 +127,13 @@ export declare class DirectorStateCheckpoint {
127
127
  * checkpoint continues from there.
128
128
  */
129
129
  resume(snapshot: DirectorStateSnapshot): void;
130
+ /**
131
+ * After resume, pin the live spawn ceiling from the current profile/flag
132
+ * while preserving `spawnCount` (cumulative used budget). Checkpoint
133
+ * metadata previously stored a historical `maxSpawns` that can diverge
134
+ * from the live runtime ceiling — operators need the live value to win.
135
+ */
136
+ applyLiveMaxSpawns(maxSpawns: number | undefined): void;
130
137
  current(): DirectorStateSnapshot;
131
138
  recordSpawn(sub: DirectorSubagentState, spawnCount: number): void;
132
139
  recordTaskAssigned(task: DirectorTaskState): void;
@@ -10983,11 +10983,15 @@ var FleetNotifier = class {
10983
10983
  }
10984
10984
  /** Resolve same-project WebUI ping URLs (cached briefly). Exposed for tests. */
10985
10985
  async endpoints() {
10986
+ return (await this.targets()).map((t) => t.url);
10987
+ }
10988
+ /** Ping targets with their tokens (cached briefly). */
10989
+ async targets() {
10986
10990
  const now = Date.now();
10987
- if (this.cache && now - this.cache.at < DISCOVERY_TTL_MS) return this.cache.urls;
10988
- const urls = await this.discover();
10989
- this.cache = { at: now, urls };
10990
- return urls;
10991
+ if (this.cache && now - this.cache.at < DISCOVERY_TTL_MS) return this.cache.targets;
10992
+ const targets = await this.discover();
10993
+ this.cache = { at: now, targets };
10994
+ return targets;
10991
10995
  }
10992
10996
  dispose() {
10993
10997
  this.disposed = true;
@@ -11002,8 +11006,8 @@ var FleetNotifier = class {
11002
11006
  this.cache = null;
11003
11007
  }
11004
11008
  async flush() {
11005
- const urls = await this.endpoints();
11006
- await Promise.all(urls.map((u) => this.doPost(u).catch(() => void 0)));
11009
+ const targets = await this.targets();
11010
+ await Promise.all(targets.map((t) => this.doPost(t.url, t.token).catch(() => void 0)));
11007
11011
  }
11008
11012
  async discover() {
11009
11013
  try {
@@ -11012,16 +11016,22 @@ var FleetNotifier = class {
11012
11016
  const list = Array.isArray(data?.instances) ? data.instances : [];
11013
11017
  return list.filter((i) => i && typeof i.httpPort === "number").filter((i) => i.pid !== this.selfPid).filter((i) => normRoot(i.projectRoot) === this.projectRoot).filter((i) => pidAlive2(i.pid)).map((i) => {
11014
11018
  const host = i.host === "0.0.0.0" || i.host === "::" || !i.host ? "127.0.0.1" : i.host;
11015
- return `http://${host}:${i.httpPort}/api/fleet/ping`;
11019
+ return {
11020
+ url: `http://${host}:${i.httpPort}/api/fleet/ping`,
11021
+ token: typeof i.authToken === "string" ? i.authToken : void 0
11022
+ };
11016
11023
  });
11017
11024
  } catch {
11018
11025
  return [];
11019
11026
  }
11020
11027
  }
11021
11028
  };
11022
- async function defaultPost(url) {
11029
+ async function defaultPost(url, token) {
11023
11030
  await fetch(url, {
11024
11031
  method: "POST",
11032
+ // The API requires a token on every bind (H3). Sent as a header rather
11033
+ // than a query param so it never lands in an access log or the URL.
11034
+ ...token ? { headers: { "x-ws-token": token } } : {},
11025
11035
  signal: AbortSignal.timeout(POST_TIMEOUT_MS)
11026
11036
  });
11027
11037
  }
@@ -11998,6 +12008,21 @@ var DirectorStateCheckpoint = class {
11998
12008
  resume(snapshot) {
11999
12009
  this.snapshot = snapshot;
12000
12010
  }
12011
+ /**
12012
+ * After resume, pin the live spawn ceiling from the current profile/flag
12013
+ * while preserving `spawnCount` (cumulative used budget). Checkpoint
12014
+ * metadata previously stored a historical `maxSpawns` that can diverge
12015
+ * from the live runtime ceiling — operators need the live value to win.
12016
+ */
12017
+ applyLiveMaxSpawns(maxSpawns) {
12018
+ if (this.snapshot.maxSpawns === maxSpawns) return;
12019
+ this.snapshot = {
12020
+ ...this.snapshot,
12021
+ maxSpawns
12022
+ };
12023
+ this.bumpUpdatedAt();
12024
+ this.schedule();
12025
+ }
12001
12026
  current() {
12002
12027
  return this.snapshot;
12003
12028
  }
@@ -2,7 +2,7 @@ import type { Tool } from '../types/tool.js';
2
2
  import type { FallbackManageToolOptions } from './fallback-manage-tool-options.js';
3
3
  export declare const SYSTEM_CONFIG_VIEW_TOOL_NAME = "system_config_view";
4
4
  interface SystemConfigViewInput {
5
- section?: 'all' | 'providers' | 'models' | 'fallbacks' | 'matrix' | 'agents' | 'refiner' | 'doctor' | undefined;
5
+ section?: 'all' | 'providers' | 'models' | 'fallbacks' | 'matrix' | 'agents' | 'fleet' | 'refiner' | 'doctor' | undefined;
6
6
  }
7
7
  interface SystemConfigViewOutput {
8
8
  status: 'ok';