auto-mode 0.5.0 → 0.7.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.
@@ -252,7 +252,7 @@ function safeExtend(schema, shape) {
252
252
  if (!isPlainObject(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
253
253
  return clone(schema, mergeDefs(schema._zod.def, { shape: extended(schema, shape) }));
254
254
  }
255
- function merge$1(a, b) {
255
+ function merge(a, b) {
256
256
  if (!b?._zod?.def) throw new Error("Invalid input to merge: expected an object schema. To merge a plain shape, use `.extend()`.");
257
257
  if (a._zod.def.checks?.length) throw new Error(".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.");
258
258
  const newShape = {};
@@ -5218,7 +5218,7 @@ const ZodObject = /*@__PURE__*/ $constructor("ZodObject", (inst, def) => {
5218
5218
  return safeExtend(this, incoming);
5219
5219
  },
5220
5220
  merge(other) {
5221
- return merge$1(this, other);
5221
+ return merge(this, other);
5222
5222
  },
5223
5223
  pick(mask) {
5224
5224
  return pick(this, mask);
@@ -5569,6 +5569,171 @@ async function loadClaudeRules(path) {
5569
5569
  hard_deny: (rules.autoMode?.hard_deny ?? []).filter((entry) => entry !== "$defaults")
5570
5570
  };
5571
5571
  }
5572
+ const PRESETS = {
5573
+ jev: {
5574
+ protocol: "system-one",
5575
+ baseURL: "https://api.typesafe.ai",
5576
+ model: "jev-1.13.0",
5577
+ apiKeyEnv: "TYPESAFE_API_KEY",
5578
+ reasoning: false,
5579
+ maxTokens: 3e3,
5580
+ timeoutMs: 5e3
5581
+ },
5582
+ spark: {
5583
+ baseURL: "https://api.meta.ai",
5584
+ model: "muse-spark-1.3-contributor",
5585
+ apiKeyEnv: "META_API_KEY",
5586
+ reasoning: true,
5587
+ maxTokens: 3e3,
5588
+ timeoutMs: 45e3
5589
+ },
5590
+ claude: {
5591
+ baseURL: "https://api.anthropic.com",
5592
+ model: "claude-haiku-4-5-20251001",
5593
+ apiKeyEnv: "ANTHROPIC_API_KEY",
5594
+ reasoning: true,
5595
+ maxTokens: 3e3,
5596
+ timeoutMs: 45e3
5597
+ },
5598
+ glm: {
5599
+ baseURL: "https://api.z.ai/api/anthropic",
5600
+ model: "glm-5.3-flash",
5601
+ apiKeyEnv: "ZAI_API_KEY",
5602
+ reasoning: true,
5603
+ maxTokens: 3e3,
5604
+ timeoutMs: 6e4
5605
+ }
5606
+ };
5607
+ const MESSAGES_DEFAULTS = {
5608
+ protocol: "messages",
5609
+ baseURL: "https://api.anthropic.com",
5610
+ reasoning: true,
5611
+ maxTokens: 3e3,
5612
+ timeoutMs: 45e3
5613
+ };
5614
+ function findPreset(id) {
5615
+ return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
5616
+ }
5617
+ //#endregion
5618
+ //#region src/config/normalize-config-file.ts
5619
+ function normalizeConfigFile(json, path) {
5620
+ if (!isRecord(json)) return {
5621
+ file: json,
5622
+ isLegacy: false,
5623
+ warnings: []
5624
+ };
5625
+ const legacyKeys = Object.keys(json).filter((key) => LEGACY_KEYS.has(key));
5626
+ if (legacyKeys.length === 0) return {
5627
+ file: json,
5628
+ isLegacy: false,
5629
+ warnings: []
5630
+ };
5631
+ const currentKeys = Object.keys(json).filter((key) => !LEGACY_KEYS.has(key));
5632
+ if (currentKeys.length > 0) throw new Error(`${path} mixes the old keys (${legacyKeys.join(", ")}) with the new ones (${currentKeys.join(", ")}); run \`auto-mode config migrate\` on a copy without the new keys`);
5633
+ const parsed = legacyConfigSchema.safeParse(json);
5634
+ if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
5635
+ const legacy = parsed.data;
5636
+ const warnings = [`${path} uses the old config keys (${legacyKeys.join(", ")}); run \`auto-mode config migrate\` to rewrite it`, ...legacy.transcriptEntries === void 0 ? [] : [`${path} sets transcriptEntries, which has no effect; migration drops it`]];
5637
+ return {
5638
+ file: buildFileFromLegacy(legacy, path),
5639
+ isLegacy: true,
5640
+ warnings
5641
+ };
5642
+ }
5643
+ function isRecord(value) {
5644
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5645
+ }
5646
+ const LEGACY_KEYS = /* @__PURE__ */ new Set([
5647
+ "preset",
5648
+ "provider",
5649
+ "classifierPath",
5650
+ "rulesPath",
5651
+ "transcriptEntries",
5652
+ "onFailure",
5653
+ "claudeSettingsPath",
5654
+ "minConfidence"
5655
+ ]);
5656
+ const text$1 = string().min(1);
5657
+ const positive$1 = number().positive();
5658
+ const legacyConfigSchema = strictObject({
5659
+ preset: text$1.optional(),
5660
+ provider: strictObject({
5661
+ protocol: _enum(["messages", "system-one"]).optional(),
5662
+ baseURL: text$1.optional(),
5663
+ model: text$1.optional(),
5664
+ apiKeyEnv: text$1.optional(),
5665
+ apiKeyCommand: text$1.optional(),
5666
+ reasoning: boolean().optional(),
5667
+ maxTokens: positive$1.optional(),
5668
+ timeoutMs: positive$1.optional()
5669
+ }).optional(),
5670
+ classifierPath: text$1.optional(),
5671
+ rulesPath: text$1.optional(),
5672
+ transcriptEntries: number().nonnegative().optional(),
5673
+ onFailure: _enum(["defer", "deny"]).optional(),
5674
+ claudeSettingsPath: text$1.nullable().optional(),
5675
+ minConfidence: number().min(.5).max(1).optional()
5676
+ });
5677
+ function buildFileFromLegacy(legacy, path) {
5678
+ const hasClassifier = legacy.preset !== void 0 || legacy.provider !== void 0;
5679
+ const decision = {
5680
+ ...hasClassifier ? { classifier: getLegacyClassifierID(legacy) } : {},
5681
+ ...legacy.minConfidence === void 0 ? {} : { minConfidence: legacy.minConfidence },
5682
+ ...legacy.onFailure === void 0 ? {} : { onFailure: legacy.onFailure }
5683
+ };
5684
+ const policy = {
5685
+ ...legacy.rulesPath === void 0 ? {} : { rulesPath: legacy.rulesPath },
5686
+ ...legacy.classifierPath === void 0 ? {} : { frameworkPath: legacy.classifierPath },
5687
+ ...legacy.claudeSettingsPath === void 0 ? {} : { claudeSettingsPath: legacy.claudeSettingsPath }
5688
+ };
5689
+ return {
5690
+ ...hasClassifier ? { classifiers: { [getLegacyClassifierID(legacy)]: buildClassifierEntry(getLegacyClassifierID(legacy), buildLegacyProvider(legacy, path)) } } : {},
5691
+ ...Object.keys(decision).length === 0 ? {} : { decision },
5692
+ ...Object.keys(policy).length === 0 ? {} : { policy }
5693
+ };
5694
+ }
5695
+ function getLegacyClassifierID(legacy) {
5696
+ if (legacy.preset !== void 0) return legacy.preset;
5697
+ return legacy.provider !== void 0 && legacy.provider.protocol !== "system-one" ? "spark" : "jev";
5698
+ }
5699
+ function buildLegacyProvider(legacy, path) {
5700
+ const preset = findPreset(getLegacyClassifierID(legacy));
5701
+ if (preset === void 0) throw new Error(`${path} names an unknown preset '${legacy.preset}'; known presets are ${Object.keys(PRESETS).join(", ")}`);
5702
+ const override = legacy.provider ?? {};
5703
+ return {
5704
+ protocol: override.protocol ?? preset.protocol ?? "messages",
5705
+ baseURL: override.baseURL ?? preset.baseURL,
5706
+ model: override.model ?? preset.model,
5707
+ apiKeyEnv: override.apiKeyEnv ?? preset.apiKeyEnv,
5708
+ apiKeyCommand: override.apiKeyCommand ?? preset.apiKeyCommand,
5709
+ reasoning: override.reasoning ?? preset.reasoning,
5710
+ maxTokens: override.maxTokens ?? preset.maxTokens,
5711
+ timeoutMs: override.timeoutMs ?? preset.timeoutMs
5712
+ };
5713
+ }
5714
+ const PROVIDER_FIELDS = [
5715
+ "baseURL",
5716
+ "model",
5717
+ "apiKeyEnv",
5718
+ "apiKeyCommand",
5719
+ "reasoning",
5720
+ "maxTokens",
5721
+ "timeoutMs"
5722
+ ];
5723
+ function buildClassifierEntry(id, provider) {
5724
+ const kind = pickEntryKind(id, provider);
5725
+ const base = kind === "messages" ? MESSAGES_DEFAULTS : findPreset(kind) ?? {};
5726
+ const fields = Object.fromEntries(PROVIDER_FIELDS.filter((field) => provider[field] !== void 0 && provider[field] !== base[field]).map((field) => [field, provider[field]]));
5727
+ return {
5728
+ ...kind === id ? {} : { kind },
5729
+ ...fields
5730
+ };
5731
+ }
5732
+ function pickEntryKind(id, provider) {
5733
+ const preset = findPreset(id);
5734
+ if (preset !== void 0 && (preset.protocol ?? "messages") === provider.protocol) return id;
5735
+ return provider.protocol === "system-one" ? "jev" : "messages";
5736
+ }
5572
5737
  //#endregion
5573
5738
  //#region src/config/read-api-key-from-command.ts
5574
5739
  function readApiKeyFromCommand(command, options = {}) {
@@ -5624,73 +5789,24 @@ function readApiKeyFromCommand(command, options = {}) {
5624
5789
  }
5625
5790
  //#endregion
5626
5791
  //#region src/config/config.ts
5792
+ const SCOPE_SOURCE_KINDS = [
5793
+ "cwd",
5794
+ "session",
5795
+ "globs",
5796
+ "atc"
5797
+ ];
5798
+ const DEFAULT_PROVIDER = findPreset("jev");
5799
+ if (DEFAULT_PROVIDER === void 0) throw new Error("the jev preset is missing");
5627
5800
  const DEFAULT_CONFIG = {
5628
- provider: {
5629
- protocol: "system-one",
5630
- baseURL: "https://api.typesafe.ai",
5631
- model: "jev-1.13.0",
5632
- apiKeyEnv: "TYPESAFE_API_KEY",
5633
- reasoning: false,
5634
- maxTokens: 3e3,
5635
- timeoutMs: 5e3
5636
- },
5801
+ provider: DEFAULT_PROVIDER,
5637
5802
  onFailure: "defer",
5638
5803
  minConfidence: .8
5639
5804
  };
5640
- const SPARK_PROVIDER = {
5641
- baseURL: "https://api.meta.ai",
5642
- model: "muse-spark-1.3-contributor",
5643
- apiKeyEnv: "META_API_KEY",
5644
- reasoning: true,
5645
- maxTokens: 3e3,
5646
- timeoutMs: 45e3
5647
- };
5648
- const PRESETS = {
5649
- jev: DEFAULT_CONFIG.provider,
5650
- spark: SPARK_PROVIDER,
5651
- claude: {
5652
- baseURL: "https://api.anthropic.com",
5653
- model: "claude-haiku-4-5-20251001",
5654
- apiKeyEnv: "ANTHROPIC_API_KEY",
5655
- reasoning: true,
5656
- maxTokens: 3e3,
5657
- timeoutMs: 45e3
5658
- },
5659
- glm: {
5660
- baseURL: "https://api.z.ai/api/anthropic",
5661
- model: "glm-5.3-flash",
5662
- apiKeyEnv: "ZAI_API_KEY",
5663
- reasoning: true,
5664
- maxTokens: 3e3,
5665
- timeoutMs: 6e4
5666
- }
5667
- };
5668
5805
  function resolveConfigPath() {
5669
5806
  const xdg = process.env["XDG_CONFIG_HOME"];
5670
5807
  const base = xdg !== void 0 && xdg !== "" ? xdg : join(homedir(), ".config");
5671
5808
  return join(base, "auto-mode", "config.json");
5672
5809
  }
5673
- const text = string().min(1);
5674
- const positive = number().positive();
5675
- const configFileSchema = strictObject({
5676
- preset: text.optional(),
5677
- provider: strictObject({
5678
- protocol: _enum(["messages", "system-one"]).optional(),
5679
- baseURL: text.optional(),
5680
- model: text.optional(),
5681
- apiKeyEnv: text.optional(),
5682
- apiKeyCommand: text.optional(),
5683
- reasoning: boolean().optional(),
5684
- maxTokens: positive.optional(),
5685
- timeoutMs: positive.optional()
5686
- }).optional(),
5687
- classifierPath: text.optional(),
5688
- rulesPath: text.optional(),
5689
- transcriptEntries: number().nonnegative().optional(),
5690
- onFailure: _enum(["defer", "deny"]).optional(),
5691
- claudeSettingsPath: text.nullable().optional(),
5692
- minConfidence: number().min(.5).max(1).optional()
5693
- });
5694
5810
  async function loadConfig(path = resolveConfigPath()) {
5695
5811
  let raw;
5696
5812
  try {
@@ -5705,33 +5821,141 @@ async function loadConfig(path = resolveConfigPath()) {
5705
5821
  } catch {
5706
5822
  throw new Error(`${path} is not valid JSON`);
5707
5823
  }
5824
+ const normalized = normalizeConfigFile(json, path);
5825
+ return buildConfig(normalized.file, path, normalized.warnings);
5826
+ }
5827
+ const text = string().min(1);
5828
+ const positive = number().positive();
5829
+ const configFileSchema = strictObject({
5830
+ classifiers: record(text, unknown()).optional(),
5831
+ scopeSources: record(text, unknown()).optional(),
5832
+ decision: strictObject({
5833
+ classifier: text.optional(),
5834
+ judge: text.nullable().optional(),
5835
+ minConfidence: number().min(.5).max(1).optional(),
5836
+ onFailure: _enum(["defer", "deny"]).optional()
5837
+ }).optional(),
5838
+ policy: strictObject({
5839
+ rulesPath: text.nullable().optional(),
5840
+ frameworkPath: text.nullable().optional(),
5841
+ claudeSettingsPath: text.nullable().optional()
5842
+ }).optional()
5843
+ });
5844
+ function buildConfig(json, path, legacyWarnings) {
5708
5845
  const parsed = configFileSchema.safeParse(json);
5709
5846
  if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
5710
- return merge(parsed.data, path);
5847
+ const file = parsed.data;
5848
+ const warnings = [...legacyWarnings];
5849
+ const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
5850
+ const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
5851
+ warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`));
5852
+ const decision = file.decision ?? {};
5853
+ const policy = file.policy ?? {};
5854
+ const judgeID = decision.judge ?? null;
5855
+ return {
5856
+ provider: resolveRole("classifier", decision.classifier ?? "jev", classifiers.entries, classifiers.droppedIDs, path),
5857
+ judge: judgeID === null ? null : resolveRole("judge", judgeID, classifiers.entries, classifiers.droppedIDs, path),
5858
+ scopeSources: scopeSources.entries,
5859
+ classifierPath: expandHomePath(policy.frameworkPath ?? void 0),
5860
+ rulesPath: expandHomePath(policy.rulesPath ?? void 0),
5861
+ onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
5862
+ claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
5863
+ minConfidence: decision.minConfidence ?? DEFAULT_CONFIG.minConfidence,
5864
+ warnings
5865
+ };
5711
5866
  }
5712
- function merge(file, path) {
5713
- const implicitProvider = file.provider !== void 0 && file.provider.protocol !== "system-one" ? SPARK_PROVIDER : DEFAULT_CONFIG.provider;
5714
- const preset = file.preset === void 0 ? implicitProvider : PRESETS[file.preset];
5715
- if (preset === void 0) throw new Error(`${path} names an unknown preset '${file.preset}'; known presets are ${Object.keys(PRESETS).join(", ")}`);
5716
- const override = file.provider ?? {};
5867
+ function buildRegistry(raw, parseEntry) {
5868
+ const entries = /* @__PURE__ */ new Map();
5869
+ const dropped = [];
5870
+ const droppedIDs = [];
5871
+ for (const [id, entry] of Object.entries(raw)) {
5872
+ const result = parseEntry(id, entry);
5873
+ if (typeof result === "string") {
5874
+ dropped.push(`${id} dropped: ${result}`);
5875
+ droppedIDs.push(id);
5876
+ } else entries.set(id, result);
5877
+ }
5717
5878
  return {
5718
- provider: {
5719
- protocol: override.protocol ?? preset.protocol ?? "messages",
5720
- baseURL: override.baseURL ?? preset.baseURL,
5721
- model: override.model ?? preset.model,
5722
- apiKeyEnv: override.apiKeyEnv ?? preset.apiKeyEnv,
5723
- apiKeyCommand: override.apiKeyCommand ?? preset.apiKeyCommand,
5724
- reasoning: override.reasoning ?? preset.reasoning,
5725
- maxTokens: override.maxTokens ?? preset.maxTokens,
5726
- timeoutMs: override.timeoutMs ?? preset.timeoutMs
5727
- },
5728
- classifierPath: file.classifierPath,
5729
- rulesPath: file.rulesPath,
5730
- onFailure: file.onFailure ?? DEFAULT_CONFIG.onFailure,
5731
- claudeSettingsPath: file.claudeSettingsPath,
5732
- minConfidence: file.minConfidence ?? DEFAULT_CONFIG.minConfidence
5879
+ entries: Object.fromEntries(entries),
5880
+ dropped,
5881
+ droppedIDs
5882
+ };
5883
+ }
5884
+ const CLASSIFIER_KINDS = [...Object.keys(PRESETS), "messages"];
5885
+ const classifierEntrySchema = strictObject({
5886
+ kind: text.optional(),
5887
+ baseURL: text.optional(),
5888
+ model: text.optional(),
5889
+ apiKeyEnv: text.optional(),
5890
+ apiKeyCommand: text.optional(),
5891
+ reasoning: boolean().optional(),
5892
+ maxTokens: positive.optional(),
5893
+ timeoutMs: positive.optional()
5894
+ });
5895
+ const CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
5896
+ "apiKey",
5897
+ "key",
5898
+ "token",
5899
+ "secret"
5900
+ ]);
5901
+ function parseClassifierEntry(id, entry) {
5902
+ if (typeof entry === "object" && entry !== null) {
5903
+ const credential = Object.keys(entry).find((key) => CREDENTIAL_KEYS.has(key));
5904
+ if (credential !== void 0) return `holds a literal ${credential}; name the key with apiKeyEnv or apiKeyCommand`;
5905
+ }
5906
+ const parsed = classifierEntrySchema.safeParse(entry);
5907
+ if (!parsed.success) return formatFirstIssue(parsed.error.issues);
5908
+ const kind = parsed.data.kind ?? id;
5909
+ if (!CLASSIFIER_KINDS.includes(kind)) return `unknown kind '${kind}'; known kinds are ${CLASSIFIER_KINDS.join(", ")}`;
5910
+ const base = kind === "messages" ? MESSAGES_DEFAULTS : findPreset(kind);
5911
+ const model = parsed.data.model ?? base?.model;
5912
+ if (base === void 0 || model === void 0) return `kind '${kind}' needs a model`;
5913
+ return {
5914
+ protocol: base.protocol ?? "messages",
5915
+ baseURL: parsed.data.baseURL ?? base.baseURL,
5916
+ model,
5917
+ apiKeyEnv: parsed.data.apiKeyEnv ?? base.apiKeyEnv,
5918
+ apiKeyCommand: parsed.data.apiKeyCommand ?? base.apiKeyCommand,
5919
+ reasoning: parsed.data.reasoning ?? base.reasoning,
5920
+ maxTokens: parsed.data.maxTokens ?? base.maxTokens,
5921
+ timeoutMs: parsed.data.timeoutMs ?? base.timeoutMs
5733
5922
  };
5734
5923
  }
5924
+ const scopeSourceEntrySchema = strictObject({
5925
+ kind: text.optional(),
5926
+ paths: array(text).min(1).optional()
5927
+ });
5928
+ function parseScopeSourceEntry(id, entry) {
5929
+ const parsed = scopeSourceEntrySchema.safeParse(entry);
5930
+ if (!parsed.success) return formatFirstIssue(parsed.error.issues);
5931
+ const kind = SCOPE_SOURCE_KINDS.find((known) => known === (parsed.data.kind ?? id));
5932
+ if (kind === void 0) return `unknown kind '${parsed.data.kind ?? id}'; known kinds are ${SCOPE_SOURCE_KINDS.join(", ")}`;
5933
+ if (kind === "globs") return parsed.data.paths === void 0 ? "kind 'globs' needs paths" : {
5934
+ kind,
5935
+ paths: parsed.data.paths
5936
+ };
5937
+ return parsed.data.paths === void 0 ? { kind } : `kind '${kind}' takes no paths`;
5938
+ }
5939
+ function formatFirstIssue(issues) {
5940
+ const [issue] = issues;
5941
+ if (issue === void 0) return "invalid entry";
5942
+ return issue.path.length === 0 ? issue.message : `${issue.path.map(String).join(".")}: ${issue.message}`;
5943
+ }
5944
+ function resolveRole(role, id, entries, droppedIDs, path) {
5945
+ const entry = Object.hasOwn(entries, id) ? entries[id] : void 0;
5946
+ if (entry !== void 0) return entry;
5947
+ if (droppedIDs.includes(id)) throw new Error(`${path}: decision.${role} names '${id}', whose entry was dropped`);
5948
+ if (findPreset(id) === void 0) throw new Error(`${path}: decision.${role} names '${id}', which is neither a classifiers entry nor a built-in kind (${Object.keys(PRESETS).join(", ")})`);
5949
+ return parseBuiltIn(id);
5950
+ }
5951
+ function parseBuiltIn(id) {
5952
+ const resolved = parseClassifierEntry(id, {});
5953
+ if (typeof resolved === "string") throw new TypeError(`built-in kind ${id} does not resolve: ${resolved}`);
5954
+ return resolved;
5955
+ }
5956
+ function expandHomePath(path) {
5957
+ return path?.startsWith("~/") === true ? join(homedir(), path.slice(2)) : path;
5958
+ }
5735
5959
  function resolveApiKey(provider, options = {}) {
5736
5960
  const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : process.env[provider.apiKeyEnv];
5737
5961
  if (fromEnv !== void 0 && fromEnv !== "") return Promise.resolve(fromEnv);
@@ -5739,25 +5963,27 @@ function resolveApiKey(provider, options = {}) {
5739
5963
  return readApiKeyFromCommand(provider.apiKeyCommand, options);
5740
5964
  }
5741
5965
  //#endregion
5966
+ //#region src/policy/resolve-shipped-policy-dir.ts
5967
+ function resolveShippedPolicyDir() {
5968
+ let dir = import.meta.dirname;
5969
+ for (;;) {
5970
+ if (existsSync(join(dir, "policy", "classifier.md"))) return join(dir, "policy");
5971
+ const parent = dirname(dir);
5972
+ if (parent === dir || dir === parse(dir).root) throw new Error("auto-mode cannot find its shipped policy/ directory");
5973
+ dir = parent;
5974
+ }
5975
+ }
5976
+ //#endregion
5742
5977
  //#region src/policy/load-policy.ts
5743
5978
  const RULES_MARKER = "<rules>";
5744
5979
  async function loadPolicy(paths = {}, framework = "classifier.md") {
5745
- const shipped = findShippedPolicyDir();
5980
+ const shipped = resolveShippedPolicyDir();
5746
5981
  const classifierPath = paths.classifierPath ?? join(shipped, framework);
5747
5982
  const rulesPath = paths.rulesPath ?? join(shipped, "rules.md");
5748
5983
  const [classifier, rules] = await Promise.all([readFile(classifierPath, "utf8"), readFile(rulesPath, "utf8")]);
5749
5984
  if (!classifier.includes(RULES_MARKER)) throw new Error(`${classifierPath} has no ${RULES_MARKER} line, so the rules cannot be placed`);
5750
5985
  return classifier.replace(RULES_MARKER, rules.trim());
5751
5986
  }
5752
- function findShippedPolicyDir() {
5753
- let dir = import.meta.dirname;
5754
- for (;;) {
5755
- if (existsSync(join(dir, "policy", "classifier.md"))) return join(dir, "policy");
5756
- const parent = dirname(dir);
5757
- if (parent === dir || dir === parse(dir).root) throw new Error("auto-mode cannot find its shipped policy/ directory");
5758
- dir = parent;
5759
- }
5760
- }
5761
5987
  //#endregion
5762
5988
  //#region src/model/anthropic-client.ts
5763
5989
  async function sendMessage(provider, apiKey, request) {
@@ -5946,12 +6172,38 @@ function buildEvidenceGuidance(name) {
5946
6172
  return "";
5947
6173
  }
5948
6174
  //#endregion
6175
+ //#region src/model/classify-decision-answers.ts
6176
+ function classifyDecisionAnswers(request, result, minConfidence) {
6177
+ const entries = Object.entries(request.rules).map(([id, rule]) => {
6178
+ const answer = result.answers[id];
6179
+ if (answer === void 0) throw new Error("Decision answer missing");
6180
+ return {
6181
+ rule,
6182
+ answer
6183
+ };
6184
+ });
6185
+ for (const tier of ["hard", "soft"]) for (const entry of entries) if (entry.rule.tier === tier && entry.answer.choice === "block" && entry.answer.confidence >= minConfidence && entry.answer.probabilities.block >= minConfidence) return {
6186
+ kind: "block",
6187
+ rule: entry.rule
6188
+ };
6189
+ let uncertain = null;
6190
+ for (const entry of entries) {
6191
+ const answer = entry.answer;
6192
+ if (!(answer.choice === "allow" && answer.confidence >= minConfidence && answer.probabilities.allow >= minConfidence) && (uncertain === null || answer.probabilities.block > uncertain.answer.probabilities.block)) uncertain = entry;
6193
+ }
6194
+ return uncertain === null ? { kind: "allow" } : {
6195
+ kind: "uncertain",
6196
+ rule: uncertain.rule
6197
+ };
6198
+ }
6199
+ //#endregion
5949
6200
  //#region src/model/collect-decision-contributors.ts
5950
- function collectDecisionContributors(request, result, verdict, minConfidence) {
6201
+ function collectDecisionContributors(request, result, minConfidence) {
6202
+ const decision = classifyDecisionAnswers(request, result, minConfidence);
5951
6203
  return Object.entries(request.rules).flatMap(([id, rule]) => {
5952
6204
  const answer = result.answers[id];
5953
6205
  if (answer === void 0) throw new Error("Decision answer missing");
5954
- return (verdict.kind === "deny" ? rule.name === verdict.rule : verdict.kind === "ask" && (answer.choice !== "allow" || answer.confidence < minConfidence || answer.probabilities.allow < minConfidence)) ? [{
6206
+ return (decision.kind === "block" ? rule === decision.rule : decision.kind === "uncertain" && (answer.choice !== "allow" || answer.confidence < minConfidence || answer.probabilities.allow < minConfidence)) ? [{
5955
6207
  rule: rule.source === "shipped" ? rule.name : id,
5956
6208
  source: rule.source,
5957
6209
  tier: rule.tier,
@@ -6041,89 +6293,466 @@ function parseReference(content, prefix) {
6041
6293
  return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
6042
6294
  }
6043
6295
  //#endregion
6044
- //#region src/model/pick-decision-verdict.ts
6045
- function pickDecisionVerdict(request, result, minConfidence) {
6046
- const entries = Object.entries(request.rules);
6047
- for (const tier of ["hard", "soft"]) for (const [id, rule] of entries) {
6048
- const answer = result.answers[id];
6049
- if (answer === void 0) throw new Error("Decision answer missing");
6050
- if (rule.tier === tier && answer.choice === "block" && answer.confidence >= minConfidence && answer.probabilities.block >= minConfidence) return {
6051
- kind: "deny",
6052
- rule: rule.name,
6053
- reason: `The action matches ${rule.name}.`
6054
- };
6055
- }
6056
- for (const [id] of entries) {
6057
- const answer = result.answers[id];
6058
- if (answer === void 0) throw new Error("Decision answer missing");
6059
- if (answer.choice !== "allow" || answer.confidence < minConfidence || answer.probabilities.allow < minConfidence) return { kind: "ask" };
6060
- }
6061
- return { kind: "allow" };
6062
- }
6063
- //#endregion
6064
- //#region src/model/send-decision.ts
6065
- async function sendDecision(provider, apiKey, request, signal) {
6066
- const input = {
6067
- model: provider.model,
6068
- state: { ...request.state },
6069
- questions: request.questions
6070
- };
6071
- let body = JSON.stringify(input);
6072
- for (const field of ["delegatedTask", "originalUserTask"]) {
6073
- const context = input.state.taskContext;
6074
- if (Buffer.byteLength(body) <= 1e5 || context === void 0 || context[field] === null) continue;
6075
- input.state.taskContext = {
6076
- ...context,
6077
- [field]: null,
6078
- omittedTaskContext: [...context.omittedTaskContext.filter((item) => item.field !== field), {
6079
- field,
6080
- reason: "budget"
6081
- }]
6082
- };
6083
- body = JSON.stringify(input);
6296
+ //#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
6297
+ const t = Symbol.for("@ts-pattern/matcher");
6298
+ const e = Symbol.for("@ts-pattern/isVariadic");
6299
+ const n = "@ts-pattern/anonymous-select-key";
6300
+ const r = (t) => Boolean(t && "object" == typeof t);
6301
+ const i = (e) => e && !!e[t];
6302
+ const o = (n, s, c) => {
6303
+ if (i(n)) {
6304
+ const { matched: r, selections: i } = n[t]().match(s);
6305
+ return r && i && Object.keys(i).forEach((t) => c(t, i[t])), r;
6084
6306
  }
6085
- const requestBytes = Buffer.byteLength(body);
6086
- if (requestBytes > 1e5) throw new DecisionRequestError("request-too-large", requestBytes, "Decision input exceeds 100000 bytes; refusing to truncate the action or user message");
6087
- const controller = new AbortController();
6088
- const stopRequest = () => {
6089
- controller.abort();
6090
- };
6091
- signal?.addEventListener("abort", stopRequest, { once: true });
6092
- if (signal?.aborted === true) controller.abort();
6093
- const timer = setTimeout(() => {
6094
- controller.abort();
6095
- }, provider.timeoutMs);
6096
- try {
6097
- let response;
6098
- try {
6099
- response = await fetch(`${provider.baseURL.replace(/\/$/, "")}/v1/systemone`, {
6100
- method: "POST",
6101
- signal: controller.signal,
6102
- headers: {
6103
- "content-type": "application/json",
6104
- authorization: `Bearer ${apiKey}`
6105
- },
6106
- body
6107
- });
6108
- } catch (error) {
6109
- if (error instanceof Error && error.name === "AbortError") throw new DecisionRequestError("aborted", requestBytes, "Decision request aborted", { cause: error });
6110
- throw new DecisionRequestError("network", requestBytes, "Decision API request failed");
6307
+ if (r(n)) {
6308
+ if (!r(s)) return !1;
6309
+ if (Array.isArray(n)) {
6310
+ if (!Array.isArray(s)) return !1;
6311
+ let t = [], r = [], u = [];
6312
+ for (const o of n.keys()) {
6313
+ const s = n[o];
6314
+ i(s) && s[e] ? u.push(s) : u.length ? r.push(s) : t.push(s);
6315
+ }
6316
+ if (u.length) {
6317
+ if (u.length > 1) throw new Error("Pattern error: Using `...P.array(...)` several times in a single pattern is not allowed.");
6318
+ if (s.length < t.length + r.length) return !1;
6319
+ const e = s.slice(0, t.length), n = 0 === r.length ? [] : s.slice(-r.length), i = s.slice(t.length, 0 === r.length ? Infinity : -r.length);
6320
+ return t.every((t, n) => o(t, e[n], c)) && r.every((t, e) => o(t, n[e], c)) && (0 === u.length || o(u[0], i, c));
6321
+ }
6322
+ return n.length === s.length && n.every((t, e) => o(t, s[e], c));
6111
6323
  }
6112
- if (!response.ok) throw new DecisionRequestError("http-status", requestBytes, `Decision API returned HTTP ${response.status}`);
6113
- const probability = number().min(0).max(1);
6114
- const answer = object({
6115
- type: literal("choice"),
6116
- choice: _enum([
6117
- "allow",
6118
- "block",
6119
- "ask"
6120
- ]),
6121
- confidence: probability,
6122
- probabilities: strictObject({
6123
- allow: probability,
6124
- block: probability,
6125
- ask: probability
6126
- })
6324
+ return Reflect.ownKeys(n).every((e) => {
6325
+ const r = n[e];
6326
+ return (e in s || i(u = r) && "optional" === u[t]().matcherType) && o(r, s[e], c);
6327
+ var u;
6328
+ });
6329
+ }
6330
+ return Object.is(s, n);
6331
+ };
6332
+ const s = (e) => {
6333
+ var n, o, u;
6334
+ return r(e) ? i(e) ? null != (n = null == (o = (u = e[t]()).getSelectionKeys) ? void 0 : o.call(u)) ? n : [] : Array.isArray(e) ? c(e, s) : c(Object.values(e), s) : [];
6335
+ };
6336
+ const c = (t, e) => t.reduce((t, n) => t.concat(e(n)), []);
6337
+ function a(t) {
6338
+ return Object.assign(t, {
6339
+ optional: () => h(t),
6340
+ and: (e) => d(t, e),
6341
+ or: (e) => y(t, e),
6342
+ select: (e) => void 0 === e ? v(t) : v(e, t)
6343
+ });
6344
+ }
6345
+ function h(e) {
6346
+ return a({ [t]: () => ({
6347
+ match: (t) => {
6348
+ let n = {};
6349
+ const r = (t, e) => {
6350
+ n[t] = e;
6351
+ };
6352
+ return void 0 === t ? (s(e).forEach((t) => r(t, void 0)), {
6353
+ matched: !0,
6354
+ selections: n
6355
+ }) : {
6356
+ matched: o(e, t, r),
6357
+ selections: n
6358
+ };
6359
+ },
6360
+ getSelectionKeys: () => s(e),
6361
+ matcherType: "optional"
6362
+ }) });
6363
+ }
6364
+ function d(...e) {
6365
+ return a({ [t]: () => ({
6366
+ match: (t) => {
6367
+ let n = {};
6368
+ const r = (t, e) => {
6369
+ n[t] = e;
6370
+ };
6371
+ return {
6372
+ matched: e.every((e) => o(e, t, r)),
6373
+ selections: n
6374
+ };
6375
+ },
6376
+ getSelectionKeys: () => c(e, s),
6377
+ matcherType: "and"
6378
+ }) });
6379
+ }
6380
+ function y(...e) {
6381
+ return a({ [t]: () => ({
6382
+ match: (t) => {
6383
+ let n = {};
6384
+ const r = (t, e) => {
6385
+ n[t] = e;
6386
+ };
6387
+ return c(e, s).forEach((t) => r(t, void 0)), {
6388
+ matched: e.some((e) => o(e, t, r)),
6389
+ selections: n
6390
+ };
6391
+ },
6392
+ getSelectionKeys: () => c(e, s),
6393
+ matcherType: "or"
6394
+ }) });
6395
+ }
6396
+ function p(e) {
6397
+ return { [t]: () => ({ match: (t) => ({ matched: Boolean(e(t)) }) }) };
6398
+ }
6399
+ function v(...e) {
6400
+ const r = "string" == typeof e[0] ? e[0] : void 0, i = 2 === e.length ? e[1] : "string" == typeof e[0] ? void 0 : e[0];
6401
+ return a({ [t]: () => ({
6402
+ match: (t) => {
6403
+ let e = { [null != r ? r : n]: t };
6404
+ return {
6405
+ matched: void 0 === i || o(i, t, (t, n) => {
6406
+ e[t] = n;
6407
+ }),
6408
+ selections: e
6409
+ };
6410
+ },
6411
+ getSelectionKeys: () => [null != r ? r : n].concat(void 0 === i ? [] : s(i))
6412
+ }) });
6413
+ }
6414
+ function b(t) {
6415
+ return !0;
6416
+ }
6417
+ function w(t) {
6418
+ return "number" == typeof t;
6419
+ }
6420
+ function S(t) {
6421
+ return "string" == typeof t;
6422
+ }
6423
+ function j(t) {
6424
+ return "bigint" == typeof t;
6425
+ }
6426
+ a(p(b));
6427
+ a(p(b));
6428
+ const x = (t) => Object.assign(a(t), {
6429
+ startsWith: (e) => {
6430
+ return x(d(t, (n = e, p((t) => S(t) && t.startsWith(n)))));
6431
+ var n;
6432
+ },
6433
+ endsWith: (e) => {
6434
+ return x(d(t, (n = e, p((t) => S(t) && t.endsWith(n)))));
6435
+ var n;
6436
+ },
6437
+ minLength: (e) => x(d(t, ((t) => p((e) => S(e) && e.length >= t))(e))),
6438
+ length: (e) => x(d(t, ((t) => p((e) => S(e) && e.length === t))(e))),
6439
+ maxLength: (e) => x(d(t, ((t) => p((e) => S(e) && e.length <= t))(e))),
6440
+ includes: (e) => {
6441
+ return x(d(t, (n = e, p((t) => S(t) && t.includes(n)))));
6442
+ var n;
6443
+ },
6444
+ regex: (e) => {
6445
+ return x(d(t, (n = e, p((t) => S(t) && Boolean(t.match(n))))));
6446
+ var n;
6447
+ }
6448
+ });
6449
+ x(p(S));
6450
+ const N = (t) => Object.assign(a(t), {
6451
+ between: (e, n) => N(d(t, ((t, e) => p((n) => w(n) && t <= n && e >= n))(e, n))),
6452
+ lt: (e) => N(d(t, ((t) => p((e) => w(e) && e < t))(e))),
6453
+ gt: (e) => N(d(t, ((t) => p((e) => w(e) && e > t))(e))),
6454
+ lte: (e) => N(d(t, ((t) => p((e) => w(e) && e <= t))(e))),
6455
+ gte: (e) => N(d(t, ((t) => p((e) => w(e) && e >= t))(e))),
6456
+ int: () => N(d(t, p((t) => w(t) && Number.isInteger(t)))),
6457
+ finite: () => N(d(t, p((t) => w(t) && Number.isFinite(t)))),
6458
+ positive: () => N(d(t, p((t) => w(t) && t > 0))),
6459
+ negative: () => N(d(t, p((t) => w(t) && t < 0)))
6460
+ });
6461
+ N(p(w));
6462
+ const k = (t) => Object.assign(a(t), {
6463
+ between: (e, n) => k(d(t, ((t, e) => p((n) => j(n) && t <= n && e >= n))(e, n))),
6464
+ lt: (e) => k(d(t, ((t) => p((e) => j(e) && e < t))(e))),
6465
+ gt: (e) => k(d(t, ((t) => p((e) => j(e) && e > t))(e))),
6466
+ lte: (e) => k(d(t, ((t) => p((e) => j(e) && e <= t))(e))),
6467
+ gte: (e) => k(d(t, ((t) => p((e) => j(e) && e >= t))(e))),
6468
+ positive: () => k(d(t, p((t) => j(t) && t > 0))),
6469
+ negative: () => k(d(t, p((t) => j(t) && t < 0)))
6470
+ });
6471
+ k(p(j)), a(p(function(t) {
6472
+ return "boolean" == typeof t;
6473
+ })), a(p(function(t) {
6474
+ return "symbol" == typeof t;
6475
+ })), a(p(function(t) {
6476
+ return null == t;
6477
+ })), a(p(function(t) {
6478
+ return null != t;
6479
+ }));
6480
+ var I = class extends Error {
6481
+ constructor(t) {
6482
+ let e;
6483
+ try {
6484
+ e = JSON.stringify(t);
6485
+ } catch (n) {
6486
+ e = t;
6487
+ }
6488
+ super(`Pattern matching error: no pattern matches value ${e}`), this.input = void 0, this.input = t;
6489
+ }
6490
+ };
6491
+ const L = {
6492
+ matched: !1,
6493
+ value: void 0
6494
+ };
6495
+ function M(t) {
6496
+ return new R(t, L);
6497
+ }
6498
+ var R = class R {
6499
+ constructor(t, e) {
6500
+ this.input = void 0, this.state = void 0, this.input = t, this.state = e;
6501
+ }
6502
+ with(...t) {
6503
+ if (this.state.matched) return this;
6504
+ const e = t[t.length - 1], r = [t[0]];
6505
+ let i;
6506
+ 3 === t.length && "function" == typeof t[1] ? i = t[1] : t.length > 2 && r.push(...t.slice(1, t.length - 1));
6507
+ let s = !1, c = {};
6508
+ const u = (t, e) => {
6509
+ s = !0, c[t] = e;
6510
+ }, a = !r.some((t) => o(t, this.input, u)) || i && !Boolean(i(this.input)) ? L : {
6511
+ matched: !0,
6512
+ value: e(s ? n in c ? c[n] : c : this.input, this.input)
6513
+ };
6514
+ return new R(this.input, a);
6515
+ }
6516
+ when(t, e) {
6517
+ if (this.state.matched) return this;
6518
+ const n = Boolean(t(this.input));
6519
+ return new R(this.input, n ? {
6520
+ matched: !0,
6521
+ value: e(this.input, this.input)
6522
+ } : L);
6523
+ }
6524
+ otherwise(t) {
6525
+ return this.state.matched ? this.state.value : t(this.input);
6526
+ }
6527
+ exhaustive(t = F) {
6528
+ return this.state.matched ? this.state.value : t(this.input);
6529
+ }
6530
+ run() {
6531
+ return this.exhaustive();
6532
+ }
6533
+ returnType() {
6534
+ return this;
6535
+ }
6536
+ narrow() {
6537
+ return this;
6538
+ }
6539
+ };
6540
+ function F(t) {
6541
+ throw new I(t);
6542
+ }
6543
+ //#endregion
6544
+ //#region src/policy/deny-reasons.ts
6545
+ const DENY_REASONS = {
6546
+ "Data Exfiltration": {
6547
+ tier: "hard",
6548
+ harm: "sending sensitive data, such as credentials or private code, off the machine",
6549
+ clears: "No instruction or configuration clears this rule."
6550
+ },
6551
+ "Secret Persistence": {
6552
+ tier: "hard",
6553
+ harm: "writing a credential in plain text into a tracked file, an image, CI configuration, or a shipped log",
6554
+ clears: "No instruction or configuration clears this rule."
6555
+ },
6556
+ "Policy Tampering": {
6557
+ tier: "hard",
6558
+ harm: "changing auto-mode's own configuration, policy, or hook, or the settings that decide whether it runs",
6559
+ clears: "Only a direct user message that explains why this match is wrong clears it; bare consent does not."
6560
+ },
6561
+ "Audit Tampering": {
6562
+ tier: "hard",
6563
+ harm: "deleting or rewriting logs, shell history, audit records, or git history to hide what happened",
6564
+ clears: "Only a direct user message that explains why this match is wrong clears it; bare consent does not."
6565
+ },
6566
+ "Destructive Payload": {
6567
+ tier: "hard",
6568
+ harm: "writing or running code whose purpose is damage or spread",
6569
+ clears: "No instruction or configuration clears this rule."
6570
+ },
6571
+ "Irreversible Deletion": {
6572
+ tier: "soft",
6573
+ harm: "deleting data that no backup, remote, or build step can restore",
6574
+ clears: "A direct user instruction that asks for this deletion and names the path clears it."
6575
+ },
6576
+ "Uncommitted Work Destruction": {
6577
+ tier: "soft",
6578
+ harm: "discarding working-tree changes that exist nowhere else",
6579
+ clears: "A direct user instruction that says those changes go away clears it."
6580
+ },
6581
+ "History Rewrite": {
6582
+ tier: "soft",
6583
+ harm: "rewriting history that others may already hold",
6584
+ clears: "A direct user instruction that names the force push, the rewrite, or the remote branch clears it."
6585
+ },
6586
+ "Default Branch Write": {
6587
+ tier: "soft",
6588
+ harm: "committing, pushing, or merging directly to the default branch",
6589
+ clears: "A direct user instruction that names the branch clears it."
6590
+ },
6591
+ "Publish and Release": {
6592
+ tier: "soft",
6593
+ harm: "putting an artifact where other people install it",
6594
+ clears: "A direct user instruction that says publish, release, or deploy and names the target clears it."
6595
+ },
6596
+ "Outbound Communication": {
6597
+ tier: "soft",
6598
+ harm: "sending content that reaches another person",
6599
+ clears: "A direct user instruction that says the message is sent, and to whom or where, clears it."
6600
+ },
6601
+ "Remote Code Execution": {
6602
+ tier: "soft",
6603
+ harm: "running code fetched from a source the user did not name",
6604
+ clears: "A direct user instruction that names the source clears it."
6605
+ },
6606
+ "Obfuscated Command": {
6607
+ tier: "soft",
6608
+ harm: "running a command whose payload cannot be read",
6609
+ clears: "A direct user message that supplies the decoded command or states what it runs clears it."
6610
+ },
6611
+ "Network Exposure": {
6612
+ tier: "soft",
6613
+ harm: "making this machine or its files reachable from outside it",
6614
+ clears: "A direct user instruction that names the service and says it becomes reachable clears it."
6615
+ },
6616
+ "Unnamed Destination": {
6617
+ tier: "soft",
6618
+ harm: "sending content to a network destination the user never named",
6619
+ clears: "A direct user instruction that names the destination clears it."
6620
+ },
6621
+ "Shared Infrastructure": {
6622
+ tier: "soft",
6623
+ harm: "changing a cluster, a cloud account, a remote host, or a repository's settings",
6624
+ clears: "A direct user instruction that names the environment or the resource clears it."
6625
+ },
6626
+ "Destructive Database Operation": {
6627
+ tier: "soft",
6628
+ harm: "deleting or overwriting stored data wholesale in a database that is not local",
6629
+ clears: "A direct user instruction that names the table or the database, and the operation, clears it."
6630
+ },
6631
+ "Persistent Configuration": {
6632
+ tier: "soft",
6633
+ harm: "a change that outlives the session or widens access on this machine",
6634
+ clears: "A direct user instruction that names the file or the setting clears it."
6635
+ },
6636
+ "Credential Exploration": {
6637
+ tier: "soft",
6638
+ harm: "reading or searching for credentials the task never called for",
6639
+ clears: "A direct user instruction that names the file or the search clears it."
6640
+ },
6641
+ "Interrupted Action Retry": {
6642
+ tier: "soft",
6643
+ harm: "running again an action the user interrupted",
6644
+ clears: "A direct user instruction that asks for the action to run again clears it."
6645
+ },
6646
+ "Security Control Removal": {
6647
+ tier: "soft",
6648
+ harm: "removing a control that protects someone other than the agent",
6649
+ clears: "A direct user instruction that names the control clears it."
6650
+ },
6651
+ "Mass Modification": {
6652
+ tier: "soft",
6653
+ harm: "changing many things at once with no way to restore them",
6654
+ clears: "A direct user instruction that names the scope clears it."
6655
+ }
6656
+ };
6657
+ //#endregion
6658
+ //#region src/policy/build-deny-reason.ts
6659
+ function buildDenyReason(rule, basis) {
6660
+ const template = getReasonTemplate(rule);
6661
+ return `${basis === "matched" ? `The action matches this rule: ${template.harm}.` : `The supplied evidence cannot rule out ${template.harm}.`} ${template.clears}`;
6662
+ }
6663
+ function getReasonTemplate(rule) {
6664
+ return M(rule.source).with("shipped", () => {
6665
+ const template = DENY_REASONS[rule.name];
6666
+ if (template === void 0) throw new Error(`No deny reason for the shipped rule ${rule.name}`);
6667
+ return template;
6668
+ }).with("configured", () => rule.tier === "hard" ? {
6669
+ harm: "an action that a hard deny entry in the user's auto-mode configuration covers",
6670
+ clears: "Only a change to that configuration clears it."
6671
+ } : {
6672
+ harm: "an action that a soft deny entry in the user's auto-mode configuration covers",
6673
+ clears: "A direct user instruction that asks for this specific action clears it."
6674
+ }).with("replacement", () => ({
6675
+ harm: "the harm this rule of the configured replacement policy describes",
6676
+ clears: "The replacement policy defines what clears it."
6677
+ })).exhaustive();
6678
+ }
6679
+ //#endregion
6680
+ //#region src/model/pick-decision-verdict.ts
6681
+ function pickDecisionVerdict(request, result, minConfidence) {
6682
+ return M(classifyDecisionAnswers(request, result, minConfidence)).with({ kind: "allow" }, () => ({ kind: "allow" })).with({ kind: "block" }, (decision) => ({
6683
+ kind: "deny",
6684
+ rule: decision.rule.name,
6685
+ reason: buildDenyReason(decision.rule, "matched")
6686
+ })).with({ kind: "uncertain" }, (decision) => ({
6687
+ kind: "deny",
6688
+ rule: decision.rule.name,
6689
+ reason: buildDenyReason(decision.rule, "unresolved")
6690
+ })).exhaustive();
6691
+ }
6692
+ //#endregion
6693
+ //#region src/model/send-decision.ts
6694
+ async function sendDecision(provider, apiKey, request, signal) {
6695
+ const input = {
6696
+ model: provider.model,
6697
+ state: { ...request.state },
6698
+ questions: request.questions
6699
+ };
6700
+ let body = JSON.stringify(input);
6701
+ for (const field of ["delegatedTask", "originalUserTask"]) {
6702
+ const context = input.state.taskContext;
6703
+ if (Buffer.byteLength(body) <= 1e5 || context === void 0 || context[field] === null) continue;
6704
+ input.state.taskContext = {
6705
+ ...context,
6706
+ [field]: null,
6707
+ omittedTaskContext: [...context.omittedTaskContext.filter((item) => item.field !== field), {
6708
+ field,
6709
+ reason: "budget"
6710
+ }]
6711
+ };
6712
+ body = JSON.stringify(input);
6713
+ }
6714
+ const requestBytes = Buffer.byteLength(body);
6715
+ if (requestBytes > 1e5) throw new DecisionRequestError("request-too-large", requestBytes, "Decision input exceeds 100000 bytes; refusing to truncate the action or user message");
6716
+ const controller = new AbortController();
6717
+ const stopRequest = () => {
6718
+ controller.abort();
6719
+ };
6720
+ signal?.addEventListener("abort", stopRequest, { once: true });
6721
+ if (signal?.aborted === true) controller.abort();
6722
+ const timer = setTimeout(() => {
6723
+ controller.abort();
6724
+ }, provider.timeoutMs);
6725
+ try {
6726
+ let response;
6727
+ try {
6728
+ response = await fetch(`${provider.baseURL.replace(/\/$/, "")}/v1/systemone`, {
6729
+ method: "POST",
6730
+ signal: controller.signal,
6731
+ headers: {
6732
+ "content-type": "application/json",
6733
+ authorization: `Bearer ${apiKey}`
6734
+ },
6735
+ body
6736
+ });
6737
+ } catch (error) {
6738
+ if (error instanceof Error && error.name === "AbortError") throw new DecisionRequestError("aborted", requestBytes, "Decision request aborted", { cause: error });
6739
+ throw new DecisionRequestError("network", requestBytes, "Decision API request failed");
6740
+ }
6741
+ if (!response.ok) throw new DecisionRequestError("http-status", requestBytes, `Decision API returned HTTP ${response.status}`);
6742
+ const probability = number().min(0).max(1);
6743
+ const answer = object({
6744
+ type: literal("choice"),
6745
+ choice: _enum([
6746
+ "allow",
6747
+ "block",
6748
+ "ask"
6749
+ ]),
6750
+ confidence: probability,
6751
+ probabilities: strictObject({
6752
+ allow: probability,
6753
+ block: probability,
6754
+ ask: probability
6755
+ })
6127
6756
  });
6128
6757
  const schema = object({
6129
6758
  model: string().min(1),
@@ -6208,7 +6837,7 @@ async function classifyWithJev(payload, config, options = {}) {
6208
6837
  requestBytes: result.requestBytes,
6209
6838
  elapsedMs: Math.round(performance.now() - start),
6210
6839
  minConfidence,
6211
- contributors: collectDecisionContributors(request, result, verdict, minConfidence)
6840
+ contributors: collectDecisionContributors(request, result, minConfidence)
6212
6841
  },
6213
6842
  note: formatClassifierNote(`${config.provider.model}: ${verdict.kind} (${Math.round(performance.now() - start)}ms, ${result.inputTokens} input tokens)`, key)
6214
6843
  };
@@ -6260,10 +6889,20 @@ function parseModelVerdict(text) {
6260
6889
  const rule = RULE.exec(text)?.[1]?.trim();
6261
6890
  if (rule === void 0 || rule === "") return { kind: "allow" };
6262
6891
  const stripped = (REASON.exec(text)?.[1]?.trim())?.replace(/^\[[^\]]*\]\s*/, "") ?? "";
6892
+ if (stripped !== "") return {
6893
+ kind: "deny",
6894
+ rule,
6895
+ reason: stripped
6896
+ };
6897
+ const template = DENY_REASONS[rule];
6263
6898
  return {
6264
6899
  kind: "deny",
6265
6900
  rule,
6266
- reason: stripped === "" ? "the policy blocks this action." : stripped
6901
+ reason: template === void 0 ? "The action matches this rule." : buildDenyReason({
6902
+ name: rule,
6903
+ tier: template.tier,
6904
+ source: "shipped"
6905
+ }, "matched")
6267
6906
  };
6268
6907
  }
6269
6908
  //#endregion
@@ -6321,6 +6960,11 @@ function toMessage(error) {
6321
6960
  return error instanceof Error ? error.message : String(error);
6322
6961
  }
6323
6962
  //#endregion
6963
+ //#region src/policy/read-denial-guidance.ts
6964
+ async function readDenialGuidance() {
6965
+ return (await readFile(join(resolveShippedPolicyDir(), "denial.md"), "utf8")).trim();
6966
+ }
6967
+ //#endregion
6324
6968
  //#region node_modules/.bun/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.js
6325
6969
  var isProduction = process.env.NODE_ENV === "production";
6326
6970
  var prefix = "Invariant failed";
@@ -6557,12 +7201,9 @@ async function classifyAction(request, config, options = {}) {
6557
7201
  if (config.provider.protocol === "system-one") try {
6558
7202
  configured = await loadClaudeRules(config.claudeSettingsPath);
6559
7203
  } catch {
7204
+ const guidance = config.onFailure === "deny" ? await tryReadDenialGuidance() : null;
6560
7205
  return {
6561
- verdict: config.onFailure === "deny" ? {
6562
- kind: "deny",
6563
- rule: "Classifier Unavailable",
6564
- reason: "Claude settings unreadable"
6565
- } : null,
7206
+ verdict: config.onFailure === "deny" ? buildGuidedDeny("Classifier Unavailable", "Claude settings unreadable.", guidance) : null,
6566
7207
  note: "Claude settings unreadable; classifier unavailable",
6567
7208
  status: "failure",
6568
7209
  unavailable: true
@@ -6579,12 +7220,28 @@ async function classifyAction(request, config, options = {}) {
6579
7220
  note: `${request.toolName} needs the model tier, which this run skipped`,
6580
7221
  status: "skipped"
6581
7222
  };
6582
- const outcome = await classifyWithModel(request, config, options);
7223
+ const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, options), tryReadDenialGuidance()]);
6583
7224
  return {
6584
7225
  ...outcome,
7226
+ verdict: outcome.verdict?.kind === "deny" ? buildGuidedDeny(outcome.verdict.rule, outcome.verdict.reason, guidance) : outcome.verdict,
6585
7227
  status: outcome.diagnostics?.status ?? outcome.verdict?.kind ?? "failure"
6586
7228
  };
6587
7229
  }
7230
+ function buildGuidedDeny(rule, reason, guidance) {
7231
+ const sentence = /[.!?]$/u.test(reason) ? reason : `${reason}.`;
7232
+ return {
7233
+ kind: "deny",
7234
+ rule,
7235
+ reason: guidance === null ? sentence : `${sentence} ${guidance}`
7236
+ };
7237
+ }
7238
+ async function tryReadDenialGuidance() {
7239
+ try {
7240
+ return await readDenialGuidance();
7241
+ } catch {
7242
+ return null;
7243
+ }
7244
+ }
6588
7245
  //#endregion
6589
7246
  //#region src/request/parse-action-request.ts
6590
7247
  const userTaskSchema = strictObject({
@@ -6635,260 +7292,12 @@ function parseActionRequest(body) {
6635
7292
  };
6636
7293
  }
6637
7294
  //#endregion
6638
- //#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
6639
- const t = Symbol.for("@ts-pattern/matcher");
6640
- const e = Symbol.for("@ts-pattern/isVariadic");
6641
- const n = "@ts-pattern/anonymous-select-key";
6642
- const r = (t) => Boolean(t && "object" == typeof t);
6643
- const i = (e) => e && !!e[t];
6644
- const o = (n, s, c) => {
6645
- if (i(n)) {
6646
- const { matched: r, selections: i } = n[t]().match(s);
6647
- return r && i && Object.keys(i).forEach((t) => c(t, i[t])), r;
6648
- }
6649
- if (r(n)) {
6650
- if (!r(s)) return !1;
6651
- if (Array.isArray(n)) {
6652
- if (!Array.isArray(s)) return !1;
6653
- let t = [], r = [], u = [];
6654
- for (const o of n.keys()) {
6655
- const s = n[o];
6656
- i(s) && s[e] ? u.push(s) : u.length ? r.push(s) : t.push(s);
6657
- }
6658
- if (u.length) {
6659
- if (u.length > 1) throw new Error("Pattern error: Using `...P.array(...)` several times in a single pattern is not allowed.");
6660
- if (s.length < t.length + r.length) return !1;
6661
- const e = s.slice(0, t.length), n = 0 === r.length ? [] : s.slice(-r.length), i = s.slice(t.length, 0 === r.length ? Infinity : -r.length);
6662
- return t.every((t, n) => o(t, e[n], c)) && r.every((t, e) => o(t, n[e], c)) && (0 === u.length || o(u[0], i, c));
6663
- }
6664
- return n.length === s.length && n.every((t, e) => o(t, s[e], c));
6665
- }
6666
- return Reflect.ownKeys(n).every((e) => {
6667
- const r = n[e];
6668
- return (e in s || i(u = r) && "optional" === u[t]().matcherType) && o(r, s[e], c);
6669
- var u;
6670
- });
6671
- }
6672
- return Object.is(s, n);
6673
- };
6674
- const s = (e) => {
6675
- var n, o, u;
6676
- return r(e) ? i(e) ? null != (n = null == (o = (u = e[t]()).getSelectionKeys) ? void 0 : o.call(u)) ? n : [] : Array.isArray(e) ? c(e, s) : c(Object.values(e), s) : [];
6677
- };
6678
- const c = (t, e) => t.reduce((t, n) => t.concat(e(n)), []);
6679
- function a(t) {
6680
- return Object.assign(t, {
6681
- optional: () => h(t),
6682
- and: (e) => d(t, e),
6683
- or: (e) => y(t, e),
6684
- select: (e) => void 0 === e ? v(t) : v(e, t)
6685
- });
6686
- }
6687
- function h(e) {
6688
- return a({ [t]: () => ({
6689
- match: (t) => {
6690
- let n = {};
6691
- const r = (t, e) => {
6692
- n[t] = e;
6693
- };
6694
- return void 0 === t ? (s(e).forEach((t) => r(t, void 0)), {
6695
- matched: !0,
6696
- selections: n
6697
- }) : {
6698
- matched: o(e, t, r),
6699
- selections: n
6700
- };
6701
- },
6702
- getSelectionKeys: () => s(e),
6703
- matcherType: "optional"
6704
- }) });
6705
- }
6706
- function d(...e) {
6707
- return a({ [t]: () => ({
6708
- match: (t) => {
6709
- let n = {};
6710
- const r = (t, e) => {
6711
- n[t] = e;
6712
- };
6713
- return {
6714
- matched: e.every((e) => o(e, t, r)),
6715
- selections: n
6716
- };
6717
- },
6718
- getSelectionKeys: () => c(e, s),
6719
- matcherType: "and"
6720
- }) });
6721
- }
6722
- function y(...e) {
6723
- return a({ [t]: () => ({
6724
- match: (t) => {
6725
- let n = {};
6726
- const r = (t, e) => {
6727
- n[t] = e;
6728
- };
6729
- return c(e, s).forEach((t) => r(t, void 0)), {
6730
- matched: e.some((e) => o(e, t, r)),
6731
- selections: n
6732
- };
6733
- },
6734
- getSelectionKeys: () => c(e, s),
6735
- matcherType: "or"
6736
- }) });
6737
- }
6738
- function p(e) {
6739
- return { [t]: () => ({ match: (t) => ({ matched: Boolean(e(t)) }) }) };
6740
- }
6741
- function v(...e) {
6742
- const r = "string" == typeof e[0] ? e[0] : void 0, i = 2 === e.length ? e[1] : "string" == typeof e[0] ? void 0 : e[0];
6743
- return a({ [t]: () => ({
6744
- match: (t) => {
6745
- let e = { [null != r ? r : n]: t };
6746
- return {
6747
- matched: void 0 === i || o(i, t, (t, n) => {
6748
- e[t] = n;
6749
- }),
6750
- selections: e
6751
- };
6752
- },
6753
- getSelectionKeys: () => [null != r ? r : n].concat(void 0 === i ? [] : s(i))
6754
- }) });
6755
- }
6756
- function b(t) {
6757
- return !0;
6758
- }
6759
- function w(t) {
6760
- return "number" == typeof t;
6761
- }
6762
- function S(t) {
6763
- return "string" == typeof t;
6764
- }
6765
- function j(t) {
6766
- return "bigint" == typeof t;
6767
- }
6768
- a(p(b));
6769
- a(p(b));
6770
- const x = (t) => Object.assign(a(t), {
6771
- startsWith: (e) => {
6772
- return x(d(t, (n = e, p((t) => S(t) && t.startsWith(n)))));
6773
- var n;
6774
- },
6775
- endsWith: (e) => {
6776
- return x(d(t, (n = e, p((t) => S(t) && t.endsWith(n)))));
6777
- var n;
6778
- },
6779
- minLength: (e) => x(d(t, ((t) => p((e) => S(e) && e.length >= t))(e))),
6780
- length: (e) => x(d(t, ((t) => p((e) => S(e) && e.length === t))(e))),
6781
- maxLength: (e) => x(d(t, ((t) => p((e) => S(e) && e.length <= t))(e))),
6782
- includes: (e) => {
6783
- return x(d(t, (n = e, p((t) => S(t) && t.includes(n)))));
6784
- var n;
6785
- },
6786
- regex: (e) => {
6787
- return x(d(t, (n = e, p((t) => S(t) && Boolean(t.match(n))))));
6788
- var n;
6789
- }
6790
- });
6791
- x(p(S));
6792
- const N = (t) => Object.assign(a(t), {
6793
- between: (e, n) => N(d(t, ((t, e) => p((n) => w(n) && t <= n && e >= n))(e, n))),
6794
- lt: (e) => N(d(t, ((t) => p((e) => w(e) && e < t))(e))),
6795
- gt: (e) => N(d(t, ((t) => p((e) => w(e) && e > t))(e))),
6796
- lte: (e) => N(d(t, ((t) => p((e) => w(e) && e <= t))(e))),
6797
- gte: (e) => N(d(t, ((t) => p((e) => w(e) && e >= t))(e))),
6798
- int: () => N(d(t, p((t) => w(t) && Number.isInteger(t)))),
6799
- finite: () => N(d(t, p((t) => w(t) && Number.isFinite(t)))),
6800
- positive: () => N(d(t, p((t) => w(t) && t > 0))),
6801
- negative: () => N(d(t, p((t) => w(t) && t < 0)))
6802
- });
6803
- N(p(w));
6804
- const k = (t) => Object.assign(a(t), {
6805
- between: (e, n) => k(d(t, ((t, e) => p((n) => j(n) && t <= n && e >= n))(e, n))),
6806
- lt: (e) => k(d(t, ((t) => p((e) => j(e) && e < t))(e))),
6807
- gt: (e) => k(d(t, ((t) => p((e) => j(e) && e > t))(e))),
6808
- lte: (e) => k(d(t, ((t) => p((e) => j(e) && e <= t))(e))),
6809
- gte: (e) => k(d(t, ((t) => p((e) => j(e) && e >= t))(e))),
6810
- positive: () => k(d(t, p((t) => j(t) && t > 0))),
6811
- negative: () => k(d(t, p((t) => j(t) && t < 0)))
6812
- });
6813
- k(p(j)), a(p(function(t) {
6814
- return "boolean" == typeof t;
6815
- })), a(p(function(t) {
6816
- return "symbol" == typeof t;
6817
- })), a(p(function(t) {
6818
- return null == t;
6819
- })), a(p(function(t) {
6820
- return null != t;
6821
- }));
6822
- var I = class extends Error {
6823
- constructor(t) {
6824
- let e;
6825
- try {
6826
- e = JSON.stringify(t);
6827
- } catch (n) {
6828
- e = t;
6829
- }
6830
- super(`Pattern matching error: no pattern matches value ${e}`), this.input = void 0, this.input = t;
6831
- }
6832
- };
6833
- const L = {
6834
- matched: !1,
6835
- value: void 0
6836
- };
6837
- function M(t) {
6838
- return new R(t, L);
6839
- }
6840
- var R = class R {
6841
- constructor(t, e) {
6842
- this.input = void 0, this.state = void 0, this.input = t, this.state = e;
6843
- }
6844
- with(...t) {
6845
- if (this.state.matched) return this;
6846
- const e = t[t.length - 1], r = [t[0]];
6847
- let i;
6848
- 3 === t.length && "function" == typeof t[1] ? i = t[1] : t.length > 2 && r.push(...t.slice(1, t.length - 1));
6849
- let s = !1, c = {};
6850
- const u = (t, e) => {
6851
- s = !0, c[t] = e;
6852
- }, a = !r.some((t) => o(t, this.input, u)) || i && !Boolean(i(this.input)) ? L : {
6853
- matched: !0,
6854
- value: e(s ? n in c ? c[n] : c : this.input, this.input)
6855
- };
6856
- return new R(this.input, a);
6857
- }
6858
- when(t, e) {
6859
- if (this.state.matched) return this;
6860
- const n = Boolean(t(this.input));
6861
- return new R(this.input, n ? {
6862
- matched: !0,
6863
- value: e(this.input, this.input)
6864
- } : L);
6865
- }
6866
- otherwise(t) {
6867
- return this.state.matched ? this.state.value : t(this.input);
6868
- }
6869
- exhaustive(t = F) {
6870
- return this.state.matched ? this.state.value : t(this.input);
6871
- }
6872
- run() {
6873
- return this.exhaustive();
6874
- }
6875
- returnType() {
6876
- return this;
6877
- }
6878
- narrow() {
6879
- return this;
6880
- }
6881
- };
6882
- function F(t) {
6883
- throw new I(t);
6884
- }
6885
- //#endregion
6886
7295
  //#region src/request/render-verdict.ts
6887
7296
  function renderVerdict(verdict) {
6888
- return M(verdict).with({ kind: "ask" }, () => null).with({ kind: "allow" }, () => JSON.stringify({ decision: "allow" })).with({ kind: "deny" }, (denied) => JSON.stringify({
7297
+ return M(verdict).with({ kind: "allow" }, () => JSON.stringify({ decision: "allow" })).with({ kind: "deny" }, (denied) => JSON.stringify({
6889
7298
  decision: "deny",
6890
7299
  reason: `[${denied.rule}] ${denied.reason}`
6891
7300
  })).exhaustive();
6892
7301
  }
6893
7302
  //#endregion
6894
- export { classifyWithModel as a, DEFAULT_CONFIG as c, resolveApiKey as d, resolveConfigPath as f, classifyLocally as i, PRESETS as l, parseActionRequest as n, parseModelVerdict as o, classifyAction as r, loadPolicy as s, renderVerdict as t, loadConfig as u };
7303
+ export { classifyWithModel as a, DEFAULT_CONFIG as c, resolveConfigPath as d, normalizeConfigFile as f, classifyLocally as i, loadConfig as l, parseActionRequest as n, parseModelVerdict as o, PRESETS as p, classifyAction as r, loadPolicy as s, renderVerdict as t, resolveApiKey as u };