@wrongstack/core 0.299.0 → 0.301.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 (165) hide show
  1. package/dist/chronicle/index.d.ts +2 -0
  2. package/dist/chronicle/index.js +297 -10
  3. package/dist/chronicle/project-server.js +253 -15
  4. package/dist/chronicle/sqlite-compaction.d.ts +20 -0
  5. package/dist/chronicle/sqlite-journal.d.ts +57 -0
  6. package/dist/coordination/agents/capability-manifest.d.ts +7 -0
  7. package/dist/coordination/agents/index.d.ts +3 -2
  8. package/dist/coordination/agents/index.js +818 -570
  9. package/dist/coordination/agents/project-agent-identity.d.ts +6 -6
  10. package/dist/coordination/agents/role-skills.d.ts +1 -1
  11. package/dist/coordination/agents/types.d.ts +6 -0
  12. package/dist/coordination/director.d.ts +10 -2
  13. package/dist/coordination/fleet-manager.d.ts +48 -3
  14. package/dist/coordination/ifleet-manager.d.ts +2 -0
  15. package/dist/coordination/index.js +6880 -6440
  16. package/dist/coordination/mail-tools.d.ts +3 -3
  17. package/dist/coordination/mailbox-project-server.js +3 -4
  18. package/dist/coordination/mailbox-types.d.ts +6 -0
  19. package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
  20. package/dist/core/agent-response.d.ts +5 -1
  21. package/dist/core/agent-tools.d.ts +3 -0
  22. package/dist/core/context.d.ts +20 -0
  23. package/dist/core/fallback-model.d.ts +48 -0
  24. package/dist/core/index.d.ts +11 -9
  25. package/dist/core/index.js +1119 -310
  26. package/dist/core/instruction-template.d.ts +83 -0
  27. package/dist/core/next-steps-slot.d.ts +88 -0
  28. package/dist/core/system-prompt-blocks.d.ts +10 -1
  29. package/dist/core/system-prompt-builder.d.ts +40 -4
  30. package/dist/core/system-prompt-memory-skills.d.ts +1 -0
  31. package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
  32. package/dist/defaults/index.js +9805 -8767
  33. package/dist/design/index.js +11 -3
  34. package/dist/execution/auto-compaction-middleware.d.ts +17 -0
  35. package/dist/execution/autonomy-brain.d.ts +15 -2
  36. package/dist/execution/brain-runtime.d.ts +3 -1
  37. package/dist/execution/compaction-core.d.ts +41 -2
  38. package/dist/execution/council-brain.d.ts +47 -1
  39. package/dist/execution/council-orchestrator.d.ts +25 -6
  40. package/dist/execution/council-prompts.d.ts +12 -1
  41. package/dist/execution/index.d.ts +11 -10
  42. package/dist/execution/index.js +10507 -9189
  43. package/dist/execution/tool-executor.d.ts +4 -1
  44. package/dist/execution/topic-shift-advisor.d.ts +53 -0
  45. package/dist/extension/index.js +8 -2
  46. package/dist/extension/registry.d.ts +8 -1
  47. package/dist/fleet-notifier.d.ts +9 -2
  48. package/dist/goal/index.js +11 -3
  49. package/dist/hooks/index.js +50 -13
  50. package/dist/hooks/runner.d.ts +12 -1
  51. package/dist/hq/index.js +59 -21
  52. package/dist/hq/protocol/fleet.d.ts +20 -0
  53. package/dist/hq/protocol.js +10 -0
  54. package/dist/hq/publisher.d.ts +21 -3
  55. package/dist/index.d.ts +22 -20
  56. package/dist/index.js +12211 -8572
  57. package/dist/infrastructure/index.js +108 -21
  58. package/dist/kernel/events/brain-events.d.ts +9 -0
  59. package/dist/kernel/events/provider-events.d.ts +42 -1
  60. package/dist/kernel/events/session-events.d.ts +13 -0
  61. package/dist/kernel/events/tool-events.d.ts +3 -3
  62. package/dist/models/index.d.ts +1 -0
  63. package/dist/models/index.js +18 -1
  64. package/dist/models/provider-credentials.d.ts +54 -0
  65. package/dist/plugin/api.d.ts +6 -0
  66. package/dist/plugin/config.d.ts +55 -0
  67. package/dist/plugin/index.d.ts +2 -1
  68. package/dist/plugin/index.js +2179 -544
  69. package/dist/plugins/auto-review-plugin.d.ts +3 -0
  70. package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
  71. package/dist/plugins/review-claim-registry.d.ts +23 -8
  72. package/dist/plugins/review-types.d.ts +2 -0
  73. package/dist/registry/index.js +109 -74
  74. package/dist/registry/tool-registry.d.ts +15 -0
  75. package/dist/security/capabilities.d.ts +2 -0
  76. package/dist/security/index.d.ts +2 -2
  77. package/dist/security/index.js +166 -44
  78. package/dist/security/permission-helpers.d.ts +23 -6
  79. package/dist/security/permission-policy.d.ts +16 -0
  80. package/dist/security/readonly-permission-policy.d.ts +20 -0
  81. package/dist/security/totp.d.ts +14 -0
  82. package/dist/session-registry-atomic-file.d.ts +32 -5
  83. package/dist/skills/frontmatter.d.ts +6 -0
  84. package/dist/skills/index.js +22 -5
  85. package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
  86. package/dist/storage/cloud-config-sync.d.ts +87 -0
  87. package/dist/storage/director-state.d.ts +7 -0
  88. package/dist/storage/index.d.ts +1 -0
  89. package/dist/storage/index.js +887 -74
  90. package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
  91. package/dist/tools/index.d.ts +1 -1
  92. package/dist/tools/index.js +898 -335
  93. package/dist/tools/one-shot-llm-tool.d.ts +1 -0
  94. package/dist/tools/plugin-manager.d.ts +27 -0
  95. package/dist/types/config/mcp-features.d.ts +18 -11
  96. package/dist/types/config/root.d.ts +8 -1
  97. package/dist/types/config/runtime.d.ts +20 -6
  98. package/dist/types/config/skills-fleet-brain.d.ts +12 -4
  99. package/dist/types/config/tools.d.ts +16 -0
  100. package/dist/types/context-window.d.ts +1 -0
  101. package/dist/types/council.d.ts +11 -0
  102. package/dist/types/hooks.d.ts +14 -0
  103. package/dist/types/index.d.ts +3 -3
  104. package/dist/types/index.js +1 -0
  105. package/dist/types/multi-agent.d.ts +12 -0
  106. package/dist/types/one-shot-llm.d.ts +25 -0
  107. package/dist/types/plugin.d.ts +28 -0
  108. package/dist/types/provider.d.ts +16 -0
  109. package/dist/types/runtime-capability-manifest.d.ts +168 -0
  110. package/dist/types/skill.d.ts +5 -0
  111. package/dist/types/system-prompt.d.ts +3 -1
  112. package/dist/types/tool-executor.d.ts +8 -1
  113. package/dist/utils/context-breakdown.d.ts +8 -2
  114. package/dist/utils/index.d.ts +1 -1
  115. package/dist/utils/index.js +141 -31
  116. package/dist/utils/regex-guard.d.ts +16 -2
  117. package/dist/utils/sage-output-block.d.ts +7 -10
  118. package/dist/utils/wstack-paths.d.ts +11 -3
  119. package/dist/worktree/index.js +4 -4
  120. package/instructions/agents/browser.md +1 -4
  121. package/instructions/agents/e2e.md +17 -15
  122. package/instructions/agents/ios.md +3 -3
  123. package/instructions/agents/search.md +1 -1
  124. package/instructions/autonomy/goal-preamble.md +4 -4
  125. package/instructions/coordination/director-preamble.md +2 -3
  126. package/instructions/coordination/subagent-baseline.md +3 -1
  127. package/instructions/leader-after-task.md +12 -0
  128. package/instructions/llm/chimera-review.md +1 -1
  129. package/instructions/modes/audit-lite.md +1 -1
  130. package/instructions/sections/tool/common-patterns.md +18 -2
  131. package/instructions/sections/tool/mailbox-compact.md +1 -1
  132. package/instructions/sections/tool/mailbox-full.md +1 -1
  133. package/instructions/system-lite.md +91 -3
  134. package/instructions/system-pro.md +296 -102
  135. package/instructions/system.md +248 -92
  136. package/package.json +3 -3
  137. package/skills/api-design/SKILL.md +3 -0
  138. package/skills/audit-log/SKILL.md +2 -0
  139. package/skills/auto-review/SKILL.md +6 -1
  140. package/skills/bug-hunter/SKILL.md +4 -1
  141. package/skills/chimera/SKILL.md +3 -0
  142. package/skills/data-governance/SKILL.md +3 -0
  143. package/skills/design-system/SKILL.md +5 -2
  144. package/skills/docker-deploy/SKILL.md +2 -0
  145. package/skills/git-flow/SKILL.md +2 -0
  146. package/skills/mailbox-bridge/SKILL.md +3 -0
  147. package/skills/mnemosyne/SKILL.md +2 -0
  148. package/skills/multi-agent/SKILL.md +4 -1
  149. package/skills/node-modern/SKILL.md +4 -1
  150. package/skills/observability/SKILL.md +3 -0
  151. package/skills/output-standards/SKILL.md +2 -0
  152. package/skills/plugin-author/SKILL.md +4 -1
  153. package/skills/prompt-engineering/SKILL.md +3 -1
  154. package/skills/react-modern/SKILL.md +6 -3
  155. package/skills/refactor-planner/SKILL.md +2 -0
  156. package/skills/research-web/SKILL.md +3 -0
  157. package/skills/sdd/SKILL.md +3 -1
  158. package/skills/security-scanner/SKILL.md +3 -0
  159. package/skills/skill-creator/SKILL.md +2 -0
  160. package/skills/tech-stack/SKILL.md +3 -0
  161. package/skills/testing/SKILL.md +4 -1
  162. package/skills/typescript-strict/SKILL.md +4 -1
  163. package/skills/wrongstack-kanban/SKILL.md +6 -3
  164. package/skills/wrongstack-mailbox/SKILL.md +4 -1
  165. package/skills/wrongstack-mailbox-mcp/SKILL.md +10 -8
@@ -47,6 +47,8 @@ var ToolCapabilities = {
47
47
  NET_OUTBOUND: "net.outbound",
48
48
  /** Can mutate in-memory session todos only. */
49
49
  SESSION_TODO: "session.todo",
50
+ /** Can park after-task `<nextsteps>` suggestions for the current turn. */
51
+ SESSION_NEXTSTEPS: "session.nextsteps",
50
52
  /** Can mutate in-memory session mode only. */
51
53
  SESSION_MODE: "session.mode",
52
54
  /** Can inspect registered tool metadata. */
@@ -712,22 +714,22 @@ var UNSAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set(["__proto__", "prototype", "
712
714
  function isRecord(value) {
713
715
  return typeof value === "object" && value !== null && !Array.isArray(value);
714
716
  }
715
- function error(diagnostics, code, path6, message) {
716
- diagnostics.push({ severity: "error", code, path: path6, message });
717
+ function error(diagnostics, code, path9, message) {
718
+ diagnostics.push({ severity: "error", code, path: path9, message });
717
719
  }
718
- function validateStringList(value, path6, diagnostics, limits) {
720
+ function validateStringList(value, path9, diagnostics, limits) {
719
721
  if (!Array.isArray(value)) {
720
- error(diagnostics, limits.listErrorCode, path6, `must be an array of strings`);
722
+ error(diagnostics, limits.listErrorCode, path9, `must be an array of strings`);
721
723
  return void 0;
722
724
  }
723
725
  if (value.length > limits.maxItems) {
724
- error(diagnostics, limits.tooManyCode, path6, `must contain at most ${limits.maxItems} ${limits.itemLabel}s`);
726
+ error(diagnostics, limits.tooManyCode, path9, `must contain at most ${limits.maxItems} ${limits.itemLabel}s`);
725
727
  return void 0;
726
728
  }
727
729
  const seen = /* @__PURE__ */ new Set();
728
730
  const out = [];
729
731
  for (const [index, entry] of value.entries()) {
730
- const itemPath = `${path6}[${index}]`;
732
+ const itemPath = `${path9}[${index}]`;
731
733
  if (typeof entry !== "string" || entry.length === 0 || entry.length > limits.maxChars) {
732
734
  error(
733
735
  diagnostics,
@@ -1467,6 +1469,7 @@ function numericValue(value) {
1467
1469
 
1468
1470
  // src/security/permission-policy.ts
1469
1471
  import * as fs from "node:fs/promises";
1472
+ import * as path6 from "node:path";
1470
1473
 
1471
1474
  // src/utils/atomic-write.ts
1472
1475
  import {
@@ -1767,19 +1770,19 @@ var UNSAFE_PROPERTY_NAMES2 = /* @__PURE__ */ new Set(["__proto__", "prototype",
1767
1770
  function isRecord2(value) {
1768
1771
  return typeof value === "object" && value !== null && !Array.isArray(value);
1769
1772
  }
1770
- function error2(diagnostics, code, path6, message) {
1771
- diagnostics.push({ severity: "error", code, path: path6, message });
1773
+ function error2(diagnostics, code, path9, message) {
1774
+ diagnostics.push({ severity: "error", code, path: path9, message });
1772
1775
  }
1773
- function validatePatterns(value, path6, diagnostics) {
1776
+ function validatePatterns(value, path9, diagnostics) {
1774
1777
  if (!Array.isArray(value)) {
1775
- error2(diagnostics, "invalid_pattern_list", path6, "must be an array of strings");
1778
+ error2(diagnostics, "invalid_pattern_list", path9, "must be an array of strings");
1776
1779
  return void 0;
1777
1780
  }
1778
1781
  if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
1779
1782
  error2(
1780
1783
  diagnostics,
1781
1784
  "too_many_patterns",
1782
- path6,
1785
+ path9,
1783
1786
  `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
1784
1787
  );
1785
1788
  return void 0;
@@ -1787,7 +1790,7 @@ function validatePatterns(value, path6, diagnostics) {
1787
1790
  const patterns = [];
1788
1791
  const seen = /* @__PURE__ */ new Set();
1789
1792
  for (const [index, pattern] of value.entries()) {
1790
- const itemPath = `${path6}[${index}]`;
1793
+ const itemPath = `${path9}[${index}]`;
1791
1794
  if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
1792
1795
  error2(
1793
1796
  diagnostics,
@@ -2201,6 +2204,19 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
2201
2204
  }
2202
2205
  };
2203
2206
 
2207
+ // src/security/permission-helpers.ts
2208
+ import { realpathSync } from "node:fs";
2209
+ import * as path5 from "node:path";
2210
+
2211
+ // src/utils/wstack-paths.ts
2212
+ import * as os from "node:os";
2213
+ import * as path4 from "node:path";
2214
+ function wstackGlobalRoot() {
2215
+ const fromEnv = process.env["WRONGSTACK_HOME"];
2216
+ if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
2217
+ return path4.join(os.homedir(), ".wrongstack");
2218
+ }
2219
+
2204
2220
  // src/security/permission-helpers.ts
2205
2221
  function matchesTrust(patterns, subject) {
2206
2222
  return patterns.includes(subject) || matchAny(patterns, subject);
@@ -2272,9 +2288,51 @@ var SHELL_READ_VERBS = /* @__PURE__ */ new Set([
2272
2288
  function stripShellQuotes(value) {
2273
2289
  return value.replace(/^['"]|['"]$/g, "");
2274
2290
  }
2291
+ var AGENT_STATE_SENSITIVE_BASENAMES = /^(?:config\.json|config\.local\.json|trust\.json|auth\.json|\.key)$/i;
2292
+ function unescapeGlobSubject(value) {
2293
+ return value.replace(/\\([*?[\]])/g, "$1");
2294
+ }
2295
+ function normalizeForCompare(value) {
2296
+ const forward = unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "");
2297
+ return process.platform === "win32" ? forward.toLowerCase() : forward;
2298
+ }
2299
+ function realpathOfNearestExisting(p) {
2300
+ let probe = p;
2301
+ const tail = [];
2302
+ for (; ; ) {
2303
+ try {
2304
+ return tail.length === 0 ? realpathSync(probe) : path5.join(realpathSync(probe), ...tail);
2305
+ } catch {
2306
+ const parent = path5.dirname(probe);
2307
+ if (parent === probe) return p;
2308
+ tail.unshift(path5.basename(probe));
2309
+ probe = parent;
2310
+ }
2311
+ }
2312
+ }
2313
+ var agentStateRootRealCache = /* @__PURE__ */ new Map();
2314
+ function isInsideAgentStateRoot(absPath) {
2315
+ const lexicalRoot = normalizeForCompare(path5.resolve(wstackGlobalRoot()));
2316
+ if (!lexicalRoot) return false;
2317
+ const target = normalizeForCompare(absPath);
2318
+ if (target === lexicalRoot || target.startsWith(`${lexicalRoot}/`)) return true;
2319
+ let realRoot = agentStateRootRealCache.get(lexicalRoot);
2320
+ if (realRoot === void 0) {
2321
+ realRoot = normalizeForCompare(realpathOfNearestExisting(path5.resolve(wstackGlobalRoot())));
2322
+ agentStateRootRealCache.set(lexicalRoot, realRoot);
2323
+ }
2324
+ if (!realRoot || realRoot === lexicalRoot) return false;
2325
+ const realTarget = normalizeForCompare(realpathOfNearestExisting(absPath));
2326
+ return realTarget === realRoot || realTarget.startsWith(`${realRoot}/`);
2327
+ }
2328
+ function isProtectedAgentStatePath(absPath) {
2329
+ if (!isInsideAgentStateRoot(absPath)) return false;
2330
+ return AGENT_STATE_SENSITIVE_BASENAMES.test(path5.basename(normalizeForCompare(absPath)));
2331
+ }
2275
2332
  function pathLooksSensitive(rawPath) {
2276
2333
  const normalized = stripShellQuotes(rawPath).replace(/\\/g, "/");
2277
- return SENSITIVE_READ_PATHS.some((pattern) => pattern.test(normalized));
2334
+ if (SENSITIVE_READ_PATHS.some((pattern) => pattern.test(normalized))) return true;
2335
+ return isProtectedAgentStatePath(normalized);
2278
2336
  }
2279
2337
  function inputPathLooksSensitive(input) {
2280
2338
  if (!input || typeof input !== "object") return false;
@@ -2298,6 +2356,34 @@ function shellCommandReadsSensitivePath(command) {
2298
2356
  }
2299
2357
 
2300
2358
  // src/security/permission-policy.ts
2359
+ function fsWriteTargetPaths(input) {
2360
+ const out = [];
2361
+ if (!input || typeof input !== "object") return out;
2362
+ const obj = input;
2363
+ for (const key of [
2364
+ "path",
2365
+ "file_path",
2366
+ "file",
2367
+ "filePath",
2368
+ "files",
2369
+ "target",
2370
+ "targetPath",
2371
+ "out",
2372
+ "directory",
2373
+ "cwd",
2374
+ "template"
2375
+ ]) {
2376
+ const value = obj[key];
2377
+ if (typeof value === "string") {
2378
+ if (value.length > 0) out.push(value);
2379
+ } else if (Array.isArray(value)) {
2380
+ for (const item of value) {
2381
+ if (typeof item === "string" && item.length > 0) out.push(item);
2382
+ }
2383
+ }
2384
+ }
2385
+ return out;
2386
+ }
2301
2387
  var DefaultPermissionPolicy = class {
2302
2388
  policy = {};
2303
2389
  loaded = false;
@@ -2383,6 +2469,30 @@ var DefaultPermissionPolicy = class {
2383
2469
  getYoloDestructive() {
2384
2470
  return this.yoloDestructive;
2385
2471
  }
2472
+ /**
2473
+ * True when this call is an FS_WRITE whose target lands inside WrongStack's
2474
+ * own state root.
2475
+ *
2476
+ * FS_WRITE tools disagree on where the target path lives: write/edit use
2477
+ * `path`, replace/format/git `files` (string OR array), patch `directory`,
2478
+ * design `out`, scaffold writes under `cwd`/`template`. Checking only
2479
+ * `path`/`file_path` let a state-root write merely switch tools, so every
2480
+ * path-bearing key is inspected.
2481
+ *
2482
+ * Shared by the YOLO carve-out and the eval-cache guard so the two cannot
2483
+ * drift: the carve-out scans all path keys while the cache key fingerprints
2484
+ * only the subject key, so the cache must consult the same predicate or a
2485
+ * secondary state-root key can be replayed a cached `auto`.
2486
+ */
2487
+ hasAgentStateWriteTarget(tool, input, ctx) {
2488
+ if (!hasCapability(tool, ToolCapabilities.FS_WRITE)) return false;
2489
+ for (const targetPath of fsWriteTargetPaths(input)) {
2490
+ const base = ctx.workingDir ?? ctx.cwd;
2491
+ const resolved = base ? path6.resolve(base, targetPath) : path6.resolve(targetPath);
2492
+ if (isInsideAgentStateRoot(resolved)) return true;
2493
+ }
2494
+ return false;
2495
+ }
2386
2496
  /**
2387
2497
  * True when YOLO is on but this specific call is a destructive shell command
2388
2498
  * the user has not opted to auto-approve. Shared by `evaluate()` and the
@@ -2390,6 +2500,7 @@ var DefaultPermissionPolicy = class {
2390
2500
  */
2391
2501
  yoloBlockedAsDestructive(tool, input, ctx) {
2392
2502
  if (!this.yolo || this.yoloDestructive) return false;
2503
+ if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
2393
2504
  const isShellSurface = tool.name === "bash" || tool.name === "exec" || (tool.capabilities ?? []).includes("shell.arbitrary");
2394
2505
  if (!isShellSurface) return false;
2395
2506
  const command = getInputString(input, "command") ?? shellCommandLineFromInput(input);
@@ -2496,7 +2607,7 @@ var DefaultPermissionPolicy = class {
2496
2607
  const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
2497
2608
  const cacheKey = `${tool.name}::${subject ?? tool.name}`;
2498
2609
  const evalKey = `${cacheKey}::${permissionFingerprint(tool)}`;
2499
- if (tool.name !== "write") {
2610
+ if (tool.name !== "write" && !this.hasAgentStateWriteTarget(tool, input, ctx)) {
2500
2611
  const cached = this._evalCache.get(evalKey);
2501
2612
  if (cached !== void 0) return cached;
2502
2613
  }
@@ -2604,7 +2715,7 @@ var DefaultPermissionPolicy = class {
2604
2715
  return decision;
2605
2716
  }
2606
2717
  if (tool.name === "write" && subject) {
2607
- if (ctx.hasRead(subject)) {
2718
+ if (ctx.hasRead(subject) && !isInsideAgentStateRoot(subject)) {
2608
2719
  return {
2609
2720
  permission: "auto",
2610
2721
  source: "context",
@@ -2954,13 +3065,14 @@ var DefaultPermissionPolicy = class {
2954
3065
  }
2955
3066
  add("yolo", false, "auto", "yolo", "YOLO mode is not active");
2956
3067
  if (tool.name === "write" && subject) {
2957
- const hasRead = ctx.hasRead(subject);
3068
+ const isAgentState = isInsideAgentStateRoot(subject);
3069
+ const hasRead = ctx.hasRead(subject) && !isAgentState;
2958
3070
  add(
2959
3071
  "write smart bypass",
2960
3072
  hasRead,
2961
3073
  "auto",
2962
3074
  "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`
3075
+ 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
3076
  );
2965
3077
  if (hasRead) {
2966
3078
  winnerIndex = steps.length - 1;
@@ -3042,7 +3154,7 @@ var DefaultPermissionPolicy = class {
3042
3154
  };
3043
3155
 
3044
3156
  // src/security/readonly-permission-policy.ts
3045
- import * as path4 from "node:path";
3157
+ import * as path7 from "node:path";
3046
3158
  var MUTATION_CAPABILITIES = /* @__PURE__ */ new Set([
3047
3159
  ToolCapabilities.FS_WRITE,
3048
3160
  ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
@@ -3056,9 +3168,11 @@ var MUTATION_CAPABILITIES = /* @__PURE__ */ new Set([
3056
3168
  ToolCapabilities.PACKAGE_INSTALL,
3057
3169
  ToolCapabilities.SUBAGENT_SPAWN
3058
3170
  ]);
3059
- function hasMutationCapability(tool) {
3171
+ function toolMutates(tool) {
3172
+ if (tool.mutating === true) return true;
3060
3173
  return (tool.capabilities ?? []).some((c) => MUTATION_CAPABILITIES.has(c));
3061
3174
  }
3175
+ var hasMutationCapability = toolMutates;
3062
3176
  function isAllowedReportFile(input, projectRoot) {
3063
3177
  if (!input || typeof input !== "object") return false;
3064
3178
  const record = input;
@@ -3066,9 +3180,9 @@ function isAllowedReportFile(input, projectRoot) {
3066
3180
  if (typeof inputPath !== "string") return false;
3067
3181
  if (!inputPath.endsWith(".md")) return false;
3068
3182
  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;
3183
+ const resolved = path7.resolve(projectRoot, inputPath);
3184
+ const tempDir = path7.resolve(projectRoot, ".temp_files");
3185
+ if (!resolved.startsWith(tempDir + path7.sep) && resolved !== tempDir) return false;
3072
3186
  return true;
3073
3187
  } catch {
3074
3188
  return false;
@@ -3091,10 +3205,12 @@ var ReadOnlyPermissionPolicy = class {
3091
3205
  if (isAllowedReportFile(input, this.projectRoot)) {
3092
3206
  return this.inner.evaluate(tool, input, ctx);
3093
3207
  }
3208
+ const matched = (tool.capabilities ?? []).filter((c) => MUTATION_CAPABILITIES.has(c));
3209
+ const why = matched.length > 0 ? `requires mutation capabilities (${matched.join(", ")})` : "is declared mutating";
3094
3210
  return {
3095
3211
  permission: "deny",
3096
3212
  source: "readonly_mode",
3097
- reason: `Session is in read-only mode: tool "${tool.name}" requires mutation capabilities (${(tool.capabilities ?? []).filter((c) => MUTATION_CAPABILITIES.has(c)).join(", ")}) which are not allowed. Only .md report files under .temp_files/ may be written.`
3213
+ reason: `Session is in read-only mode: tool "${tool.name}" ${why}, which is not allowed. Only .md report files under .temp_files/ may be written.`
3098
3214
  };
3099
3215
  }
3100
3216
  async trust(rule) {
@@ -3217,16 +3333,20 @@ function generateTotp(secretBase32, atMs = Date.now(), stepSeconds = DEFAULT_STE
3217
3333
  return hotp(secret, counter, digits);
3218
3334
  }
3219
3335
  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;
3336
+ return verifyTotpCounter(code, secretBase32, atMs, window, stepSeconds, digits) !== void 0;
3337
+ }
3338
+ function verifyTotpCounter(code, secretBase32, atMs = Date.now(), window = DEFAULT_WINDOW, stepSeconds = DEFAULT_STEP_SECONDS, digits = DEFAULT_DIGITS) {
3339
+ if (typeof code !== "string" || code.length !== digits) return void 0;
3221
3340
  const secret = base32Decode(secretBase32);
3222
3341
  const currentCounter = totpCounter(Math.floor(atMs / 1e3), stepSeconds);
3223
3342
  for (let offset = -window; offset <= window; offset++) {
3224
- const expected = hotp(secret, currentCounter + offset, digits);
3343
+ const counter = currentCounter + offset;
3344
+ const expected = hotp(secret, counter, digits);
3225
3345
  if (timingSafeEqual(Buffer.from(expected), Buffer.from(code))) {
3226
- return true;
3346
+ return counter;
3227
3347
  }
3228
3348
  }
3229
- return false;
3349
+ return void 0;
3230
3350
  }
3231
3351
  function buildOtpAuthUri(secretBase32, account, issuer = "WrongStack HQ", stepSeconds = DEFAULT_STEP_SECONDS, digits = DEFAULT_DIGITS) {
3232
3352
  const label = `${encodeURIComponent(issuer)}:${encodeURIComponent(account)}`;
@@ -3275,7 +3395,7 @@ function verifyRecoveryCode(code, storedHashes) {
3275
3395
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, scryptSync } from "node:crypto";
3276
3396
  import * as fs2 from "node:fs";
3277
3397
  import * as fsp from "node:fs/promises";
3278
- import * as path5 from "node:path";
3398
+ import * as path8 from "node:path";
3279
3399
 
3280
3400
  // src/utils/deep-merge.ts
3281
3401
  var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
@@ -3578,7 +3698,7 @@ var DefaultSecretVault = class {
3578
3698
  const oldVersion = this._keyVersion;
3579
3699
  const newKey = randomBytes2(KEY_BYTES);
3580
3700
  const newVersion = oldVersion + 1;
3581
- fs2.mkdirSync(path5.dirname(this.keyFile), { recursive: true });
3701
+ fs2.mkdirSync(path8.dirname(this.keyFile), { recursive: true });
3582
3702
  const passphrase = getVaultPassphrase();
3583
3703
  if (passphrase) {
3584
3704
  writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
@@ -3665,7 +3785,7 @@ var DefaultSecretVault = class {
3665
3785
  } catch (err) {
3666
3786
  if (err.code !== "ENOENT") throw err;
3667
3787
  }
3668
- fs2.mkdirSync(path5.dirname(this.keyFile), { recursive: true });
3788
+ fs2.mkdirSync(path8.dirname(this.keyFile), { recursive: true });
3669
3789
  const key = randomBytes2(KEY_BYTES);
3670
3790
  const passphrase = getVaultPassphrase();
3671
3791
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
@@ -3725,7 +3845,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
3725
3845
  }
3726
3846
  const merged = deepMerge(current, patch ?? {});
3727
3847
  const encrypted = encryptConfigSecrets(merged, vault);
3728
- await fsp.mkdir(path5.dirname(configPath), { recursive: true });
3848
+ await fsp.mkdir(path8.dirname(configPath), { recursive: true });
3729
3849
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
3730
3850
  await restrictFilePermissions2(configPath);
3731
3851
  await vault.flushHardening?.();
@@ -3947,33 +4067,33 @@ var RESERVED_BYPASS_FIELDS = /* @__PURE__ */ new Set([
3947
4067
  function isRecord3(value) {
3948
4068
  return typeof value === "object" && value !== null && !Array.isArray(value);
3949
4069
  }
3950
- function addString(issues, value, path6, required = true) {
4070
+ function addString(issues, value, path9, required = true) {
3951
4071
  if (typeof value === "string" && value.trim().length > 0) return true;
3952
4072
  if (!required && value === void 0) return false;
3953
- issues.push({ path: path6, message: "must be a non-empty string" });
4073
+ issues.push({ path: path9, message: "must be a non-empty string" });
3954
4074
  return false;
3955
4075
  }
3956
- function addEnum(issues, value, path6, values) {
4076
+ function addEnum(issues, value, path9, values) {
3957
4077
  if (typeof value === "string" && values.has(value)) return true;
3958
- issues.push({ path: path6, message: `must be one of: ${[...values].join(", ")}` });
4078
+ issues.push({ path: path9, message: `must be one of: ${[...values].join(", ")}` });
3959
4079
  return false;
3960
4080
  }
3961
- function validateAttributes(issues, value, path6) {
4081
+ function validateAttributes(issues, value, path9) {
3962
4082
  if (!isRecord3(value)) {
3963
- issues.push({ path: path6, message: "must be an object" });
4083
+ issues.push({ path: path9, message: "must be an object" });
3964
4084
  return false;
3965
4085
  }
3966
4086
  let valid = true;
3967
4087
  for (const [key, item] of Object.entries(value)) {
3968
4088
  if (RESERVED_BYPASS_FIELDS.has(key)) {
3969
4089
  issues.push({
3970
- path: `${path6}.${key}`,
4090
+ path: `${path9}.${key}`,
3971
4091
  message: "generic authorization bypass fields are forbidden"
3972
4092
  });
3973
4093
  valid = false;
3974
4094
  } else if (item !== null && !["string", "number", "boolean"].includes(typeof item)) {
3975
4095
  issues.push({
3976
- path: `${path6}.${key}`,
4096
+ path: `${path9}.${key}`,
3977
4097
  message: "must be a string, number, boolean, or null"
3978
4098
  });
3979
4099
  valid = false;
@@ -3981,16 +4101,16 @@ function validateAttributes(issues, value, path6) {
3981
4101
  }
3982
4102
  return valid;
3983
4103
  }
3984
- function validateScope(issues, value, path6) {
4104
+ function validateScope(issues, value, path9) {
3985
4105
  if (!isRecord3(value)) {
3986
- issues.push({ path: path6, message: "must be an object" });
4106
+ issues.push({ path: path9, message: "must be an object" });
3987
4107
  return false;
3988
4108
  }
3989
4109
  for (const field of ["projectId", "sessionId", "cwd"]) {
3990
- if (value[field] !== void 0) addString(issues, value[field], `${path6}.${field}`);
4110
+ if (value[field] !== void 0) addString(issues, value[field], `${path9}.${field}`);
3991
4111
  }
3992
4112
  if (value.constraints !== void 0)
3993
- validateAttributes(issues, value.constraints, `${path6}.constraints`);
4113
+ validateAttributes(issues, value.constraints, `${path9}.constraints`);
3994
4114
  return true;
3995
4115
  }
3996
4116
  function decodeTrustBoundaryRequest(input) {
@@ -4113,9 +4233,11 @@ export {
4113
4233
  sanitizeApiError,
4114
4234
  scrubErrorDetail,
4115
4235
  scrubErrorText,
4236
+ toolMutates,
4116
4237
  validateDirectoryPolicy,
4117
4238
  validateTrustPolicy,
4118
4239
  verifyRecoveryCode,
4119
- verifyTotp
4240
+ verifyTotp,
4241
+ verifyTotpCounter
4120
4242
  };
4121
4243
  //# 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
@@ -31,6 +31,26 @@
31
31
  import type { Context } from '../core/context.js';
32
32
  import type { PermissionDecision, PermissionPolicy, PermissionTrace } from '../types/permission.js';
33
33
  import type { Tool } from '../types/tool.js';
34
+ /**
35
+ * Does this tool change something?
36
+ *
37
+ * `capabilities` is the precise answer and stays authoritative — but it is
38
+ * OPT-IN metadata, and a repo-wide count found 154 tools declaring
39
+ * `mutating: true` of which 121 carried no mutation capability at all (28 of
40
+ * those at `permission: 'auto'`, including tools that write API keys and one
41
+ * that sends messages outbound). Read-only mode consulted only `capabilities`,
42
+ * so every one of them ran unblocked and unprompted, while
43
+ * `permission-policy.ts` was already answering the same question with
44
+ * `tool.mutating || …`.
45
+ *
46
+ * Trusting `mutating` here closes all 121 at once and makes the two policies
47
+ * agree. It fails in the safe direction: a tool that declares itself mutating
48
+ * is blocked in read-only mode even if its capability list is incomplete.
49
+ * Fixing the capability labels is still worth doing — it is what the finer
50
+ * grained gates key on — but it is no longer what stands between read-only
51
+ * mode and a write.
52
+ */
53
+ export declare function toolMutates(tool: Tool): boolean;
34
54
  /**
35
55
  * Read-only permission policy — session-scoped toggle via ctx.meta['readOnly'].
36
56
  *
@@ -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
  *
@@ -1,12 +1,39 @@
1
1
  import type { SessionRegistryEntry } from './session-registry-types.js';
2
2
  /** Also the scan interval for {@link pruneStaleTempFiles} — see its caller. */
3
3
  export declare const STALE_TMP_MS = 60000;
4
+ /** `host:pid` owner stamp written into the advisory lock on acquisition. */
5
+ export declare function lockOwnerStamp(): string;
4
6
  /**
5
- * Break a contended lock if it is stale: the recorded owner pid is no longer
6
- * alive, or the lock is older than {@link STALE_LOCK_MS}. Returns true when the
7
- * lock was removed (caller should retry acquisition). Best-effort and
8
- * race-tolerant a fresh lock (age ~0, live owner) is never broken, so the
9
- * common concurrent case self-heals on the next heartbeat.
7
+ * Unlink `lockPath` only if it still carries OUR exact host:pid stamp. A
8
+ * stale-lock breaker may have stolen the lock and re-stamped it with another
9
+ * host:pid; the resumed holder must not remove the new owner's lock. An EMPTY
10
+ * read is never ours to remove either it may be the new owner's lock in its
11
+ * open-to-stamp gap (that lock is mtime-bounded instead).
12
+ *
13
+ * Residual race: between the owner read and the unlink, a foreign-host lease
14
+ * break could replace the lock; unlinking would then remove the new owner's
15
+ * lock. Exploiting it requires a >STALE_LOCK_MS hold AND a rename+stamp
16
+ * landing inside that microsecond window, so it is accepted and bounded by
17
+ * the same foreign-lease logic that enabled it.
18
+ */
19
+ export declare function maybeUnlinkOwnedLock(lockPath: string): Promise<void>;
20
+ /**
21
+ * Break a contended lock if it is stale: a same-host owner pid that is no
22
+ * longer alive, or a foreign-host/ownerless lock older than
23
+ * {@link STALE_LOCK_MS}. A live same-host owner's lock is only broken past
24
+ * {@link SAME_HOST_STALE_MS} — a live-but-wedged holder must not degrade
25
+ * cross-session writes forever, but mtime alone must not false-positive while
26
+ * the holder is mid-critical-section. Returns true when the lock was removed
27
+ * (caller should retry acquisition).
28
+ *
29
+ * Removal is an atomic RENAME to a unique tombstone, never unlink: with
30
+ * unlink, two waiters can both read a stale lock and one's unlink can remove
31
+ * the OTHER's fresh lock, letting both into the critical section (POSIX).
32
+ * Rename wins for exactly one waiter; losers get ENOENT and simply retry.
33
+ * (A rename landing after the winner re-created the lock could still move the
34
+ * winner's fresh lock — a pre-existing microsecond TOCTOU; the caller's
35
+ * stamp-guarded cleanup prevents the resumed holder from unlinking the new
36
+ * owner's lock.)
10
37
  */
11
38
  export declare function breakStaleLock(lockPath: string): Promise<boolean>;
12
39
  export declare function writeAtomicFile(filePath: string, registry: Record<string, SessionRegistryEntry>): Promise<void>;
@@ -28,6 +28,12 @@ export interface ParsedSkillFrontmatter {
28
28
  * consult this field, so it neither grants nor restricts any tool.
29
29
  */
30
30
  allowedTools?: string[] | undefined;
31
+ /** WrongStack stable runtime capabilities required before this skill may load. */
32
+ requiredCapabilities?: string[] | undefined;
33
+ /** Exact registered tool names required before this skill may render or load. */
34
+ requiredTools?: string[] | undefined;
35
+ /** Capabilities that improve the workflow but have documented fallbacks. */
36
+ optionalCapabilities?: string[] | undefined;
31
37
  }
32
38
  /**
33
39
  * Parse the YAML frontmatter block from a raw SKILL.md file. Returns `{}` when