@astrasyncai/verification-gateway 3.10.0 → 3.11.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 (77) hide show
  1. package/dist/adapter-interface/interface.d.mts +1 -1
  2. package/dist/adapter-interface/interface.d.ts +1 -1
  3. package/dist/adapters/express.js +1 -1
  4. package/dist/adapters/express.js.map +1 -1
  5. package/dist/adapters/express.mjs +1 -1
  6. package/dist/adapters/express.mjs.map +1 -1
  7. package/dist/adapters/mcp.js +1 -1
  8. package/dist/adapters/mcp.js.map +1 -1
  9. package/dist/adapters/mcp.mjs +1 -1
  10. package/dist/adapters/mcp.mjs.map +1 -1
  11. package/dist/adapters/nextjs.js +1 -1
  12. package/dist/adapters/nextjs.js.map +1 -1
  13. package/dist/adapters/nextjs.mjs +1 -1
  14. package/dist/adapters/nextjs.mjs.map +1 -1
  15. package/dist/adapters/sdk.js +1 -1
  16. package/dist/adapters/sdk.js.map +1 -1
  17. package/dist/adapters/sdk.mjs +1 -1
  18. package/dist/adapters/sdk.mjs.map +1 -1
  19. package/dist/bin/astrasync-claude-hook.js +317 -35
  20. package/dist/bin/astrasync-codex-hook.js +5107 -0
  21. package/dist/bin/astrasync-guard.js +430 -40
  22. package/dist/bin/astrasync.js +453 -41
  23. package/dist/browser/background.js +279 -24
  24. package/dist/browser/background.js.map +1 -1
  25. package/dist/browser/background.mjs +279 -24
  26. package/dist/browser/background.mjs.map +1 -1
  27. package/dist/browser/browser-adapter.d.mts +1 -1
  28. package/dist/browser/browser-adapter.d.ts +1 -1
  29. package/dist/claude-code/claude-code-adapter.d.mts +1 -1
  30. package/dist/claude-code/claude-code-adapter.d.ts +1 -1
  31. package/dist/claude-code/claude-code-adapter.js +9 -1
  32. package/dist/claude-code/claude-code-adapter.js.map +1 -1
  33. package/dist/claude-code/claude-code-adapter.mjs +9 -1
  34. package/dist/claude-code/claude-code-adapter.mjs.map +1 -1
  35. package/dist/cli/index.d.mts +1 -1
  36. package/dist/cli/index.d.ts +1 -1
  37. package/dist/codex/index.d.mts +230 -0
  38. package/dist/codex/index.d.ts +230 -0
  39. package/dist/codex/index.js +5225 -0
  40. package/dist/codex/index.js.map +1 -0
  41. package/dist/codex/index.mjs +5197 -0
  42. package/dist/codex/index.mjs.map +1 -0
  43. package/dist/cursor/cursor-adapter.d.mts +1 -1
  44. package/dist/cursor/cursor-adapter.d.ts +1 -1
  45. package/dist/cursor/extension.d.mts +1 -1
  46. package/dist/cursor/extension.d.ts +1 -1
  47. package/dist/cursor/extension.js +279 -24
  48. package/dist/cursor/extension.js.map +1 -1
  49. package/dist/cursor/extension.mjs +279 -24
  50. package/dist/cursor/extension.mjs.map +1 -1
  51. package/dist/gateway/gateway.d.mts +32 -2
  52. package/dist/gateway/gateway.d.ts +32 -2
  53. package/dist/gateway/gateway.js +279 -24
  54. package/dist/gateway/gateway.js.map +1 -1
  55. package/dist/gateway/gateway.mjs +279 -24
  56. package/dist/gateway/gateway.mjs.map +1 -1
  57. package/dist/git-trigger/git-hooks.d.mts +1 -1
  58. package/dist/git-trigger/git-hooks.d.ts +1 -1
  59. package/dist/index.d.mts +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/index.js.map +1 -1
  63. package/dist/index.mjs +1 -1
  64. package/dist/index.mjs.map +1 -1
  65. package/dist/local-evaluator/evaluator.d.mts +1 -1
  66. package/dist/local-evaluator/evaluator.d.ts +1 -1
  67. package/dist/local-evaluator/evaluator.js +165 -7
  68. package/dist/local-evaluator/evaluator.js.map +1 -1
  69. package/dist/local-evaluator/evaluator.mjs +165 -7
  70. package/dist/local-evaluator/evaluator.mjs.map +1 -1
  71. package/dist/{types-D0lCtFQ7.d.mts → types-BV6abBnF.d.mts} +6 -0
  72. package/dist/{types-YbiXYKx1.d.ts → types-C23_uaY8.d.ts} +6 -0
  73. package/dist/verify.js +1 -1
  74. package/dist/verify.js.map +1 -1
  75. package/dist/verify.mjs +1 -1
  76. package/dist/verify.mjs.map +1 -1
  77. package/package.json +8 -2
@@ -4543,7 +4543,9 @@ var init_types = __esm({
4543
4543
  "git push",
4544
4544
  "npm publish",
4545
4545
  "docker",
4546
- "kubectl"
4546
+ "kubectl",
4547
+ "gh pr create",
4548
+ "gh pr merge"
4547
4549
  ];
4548
4550
  SENSITIVE_PATHS = [
4549
4551
  ".ssh",
@@ -4564,12 +4566,178 @@ var init_types = __esm({
4564
4566
  }
4565
4567
  });
4566
4568
 
4569
+ // src/local-evaluator/command-matcher.ts
4570
+ function buildRiskList(entries) {
4571
+ const singleWord = /* @__PURE__ */ new Set();
4572
+ const multiWord = [];
4573
+ for (const entry of entries) {
4574
+ const words = entry.toLowerCase().trim().split(/\s+/);
4575
+ if (words.length === 1) singleWord.add(words[0]);
4576
+ else multiWord.push(words);
4577
+ }
4578
+ multiWord.sort((a, b) => b.length - a.length);
4579
+ return { singleWord, multiWord };
4580
+ }
4581
+ function stripHeredocBodies(text) {
4582
+ const lines = text.split("\n");
4583
+ const kept = [];
4584
+ let terminator = null;
4585
+ for (const line of lines) {
4586
+ if (terminator !== null) {
4587
+ if (line.trim() === terminator) terminator = null;
4588
+ continue;
4589
+ }
4590
+ kept.push(line);
4591
+ const heredoc = line.match(/<<-?\s*['"]?(\w+)['"]?/);
4592
+ if (heredoc) terminator = heredoc[1];
4593
+ }
4594
+ return kept.join("\n");
4595
+ }
4596
+ function extractDollarSubstitutions(text, collect) {
4597
+ let result = text;
4598
+ let start = result.indexOf("$(");
4599
+ let iterations = 0;
4600
+ while (start !== -1 && iterations < 50) {
4601
+ iterations++;
4602
+ let depth = 0;
4603
+ let end = result.length;
4604
+ for (let i = start + 1; i < result.length; i++) {
4605
+ if (result[i] === "(") depth++;
4606
+ else if (result[i] === ")") {
4607
+ depth--;
4608
+ if (depth === 0) {
4609
+ end = i;
4610
+ break;
4611
+ }
4612
+ }
4613
+ }
4614
+ collect(result.slice(start + 2, end));
4615
+ result = `${result.slice(0, start)} ${result.slice(Math.min(end + 1, result.length))}`;
4616
+ start = result.indexOf("$(");
4617
+ }
4618
+ return result;
4619
+ }
4620
+ function extractBacktickSubstitutions(text, collect) {
4621
+ if (!text.includes("`")) return text;
4622
+ const chunks = text.split("`");
4623
+ const kept = [];
4624
+ for (let i = 0; i < chunks.length; i++) {
4625
+ if (i % 2 === 1) collect(chunks[i]);
4626
+ else kept.push(chunks[i]);
4627
+ }
4628
+ return kept.join(" ");
4629
+ }
4630
+ function basename(token) {
4631
+ const slash = token.lastIndexOf("/");
4632
+ return slash === -1 ? token : token.slice(slash + 1);
4633
+ }
4634
+ function matchSegment(segment, list) {
4635
+ const tokens = segment.split(/\s+/).filter((t) => /[a-z0-9]/.test(t));
4636
+ let i = 0;
4637
+ let hops = 0;
4638
+ while (i < tokens.length) {
4639
+ while (i < tokens.length && (tokens[i] === "export" || ASSIGN_RE.test(tokens[i]))) i++;
4640
+ if (i >= tokens.length) return null;
4641
+ const cmd = basename(tokens[i]);
4642
+ const significant = [cmd, ...tokens.slice(i + 1).filter((t) => !t.startsWith("-"))];
4643
+ for (const entry of list.multiWord) {
4644
+ if (entry.length <= significant.length && entry.every((w, j) => significant[j] === w)) {
4645
+ return entry.join(" ");
4646
+ }
4647
+ }
4648
+ if (list.singleWord.has(cmd)) return cmd;
4649
+ if (WRAPPERS.has(cmd) && hops < MAX_WRAPPER_HOPS) {
4650
+ hops++;
4651
+ i++;
4652
+ while (i < tokens.length && (tokens[i].startsWith("-") || ASSIGN_RE.test(tokens[i]))) i++;
4653
+ continue;
4654
+ }
4655
+ return null;
4656
+ }
4657
+ return null;
4658
+ }
4659
+ function collectMatches(command, list, matches, depth) {
4660
+ if (depth > MAX_RECURSION_DEPTH || command.length > MAX_COMMAND_LENGTH) return;
4661
+ let text = command.toLowerCase();
4662
+ text = stripHeredocBodies(text);
4663
+ const recurse = (inner) => collectMatches(inner, list, matches, depth + 1);
4664
+ text = extractDollarSubstitutions(text, recurse);
4665
+ text = extractBacktickSubstitutions(text, recurse);
4666
+ text = text.replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
4667
+ text = text.replace(/(^|\s)#[^\n]*/g, " ");
4668
+ const segments = text.split(/\|\||&&|[;|&\n]|[({]/);
4669
+ for (const segment of segments) {
4670
+ const matched = matchSegment(segment, list);
4671
+ if (matched) matches.add(matched);
4672
+ }
4673
+ }
4674
+ function matchHighRiskCommands(command, highRiskCommands = HIGH_RISK_COMMANDS) {
4675
+ if (!command) return [];
4676
+ const matches = /* @__PURE__ */ new Set();
4677
+ collectMatches(command, buildRiskList(highRiskCommands), matches, 0);
4678
+ return [...matches];
4679
+ }
4680
+ function isLocalhostOnlyNetworkCommand(command) {
4681
+ const text = command.toLowerCase();
4682
+ const tokens = text.split(/\s+/).map((t) => t.replace(/^["'`]+|["'`,;)]+$/g, ""));
4683
+ let localCount = 0;
4684
+ for (const token of tokens) {
4685
+ let host = null;
4686
+ if (token.includes("://")) {
4687
+ let rest = token.slice(token.indexOf("://") + 3);
4688
+ const at = rest.indexOf("@");
4689
+ if (at !== -1) rest = rest.slice(at + 1);
4690
+ host = rest.split(/[/?#]/)[0];
4691
+ } else if (/^[a-z0-9.[\]:-]+:\d+(\/.*)?$/.test(token)) {
4692
+ host = token.split("/")[0];
4693
+ } else if (/^(localhost|127\.0\.0\.1|\[?::1\]?)(\/.*)?$/.test(token)) {
4694
+ host = token.split("/")[0];
4695
+ }
4696
+ if (host === null) continue;
4697
+ const bare = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
4698
+ if (LOCAL_HOST_RE.test(bare)) {
4699
+ localCount++;
4700
+ } else {
4701
+ return false;
4702
+ }
4703
+ }
4704
+ return localCount > 0;
4705
+ }
4706
+ var WRAPPERS, ASSIGN_RE, MAX_WRAPPER_HOPS, MAX_RECURSION_DEPTH, MAX_COMMAND_LENGTH, LOCAL_HOST_RE;
4707
+ var init_command_matcher = __esm({
4708
+ "src/local-evaluator/command-matcher.ts"() {
4709
+ "use strict";
4710
+ init_types();
4711
+ WRAPPERS = /* @__PURE__ */ new Set([
4712
+ "sudo",
4713
+ "doas",
4714
+ "env",
4715
+ "nohup",
4716
+ "time",
4717
+ "timeout",
4718
+ "nice",
4719
+ "ionice",
4720
+ "stdbuf",
4721
+ "xargs",
4722
+ "command",
4723
+ "exec",
4724
+ "builtin"
4725
+ ]);
4726
+ ASSIGN_RE = /^[a-z_][a-z0-9_]*=/;
4727
+ MAX_WRAPPER_HOPS = 4;
4728
+ MAX_RECURSION_DEPTH = 3;
4729
+ MAX_COMMAND_LENGTH = 1e5;
4730
+ LOCAL_HOST_RE = /^(localhost|127\.0\.0\.1|::1|[a-z0-9.-]*\.localhost)$/;
4731
+ }
4732
+ });
4733
+
4567
4734
  // src/local-evaluator/evaluator.ts
4568
4735
  var LocalEvaluator;
4569
4736
  var init_evaluator = __esm({
4570
4737
  "src/local-evaluator/evaluator.ts"() {
4571
4738
  "use strict";
4572
4739
  init_types();
4740
+ init_command_matcher();
4573
4741
  LocalEvaluator = class {
4574
4742
  constructor(policy) {
4575
4743
  this.requestCounts = /* @__PURE__ */ new Map();
@@ -4719,12 +4887,10 @@ var init_evaluator = __esm({
4719
4887
  return score;
4720
4888
  }
4721
4889
  if (context.purpose === "shell.exec" && context.target) {
4722
- const targetLower = context.target.toLowerCase();
4723
- for (const cmd of HIGH_RISK_COMMANDS) {
4724
- if (targetLower.startsWith(cmd) || targetLower.includes(` ${cmd} `) || targetLower.includes(` ${cmd}`)) {
4725
- score = Math.max(score, 80);
4726
- break;
4727
- }
4890
+ const matched = matchHighRiskCommands(context.target);
4891
+ const localProbeOnly = matched.length > 0 && matched.every((cmd) => cmd === "curl" || cmd === "wget") && isLocalhostOnlyNetworkCommand(context.target);
4892
+ if (matched.length > 0 && !localProbeOnly) {
4893
+ score = Math.max(score, 80);
4728
4894
  }
4729
4895
  }
4730
4896
  if ((context.purpose === "file.read" || context.purpose === "file.write" || context.purpose === "file.delete") && context.target) {
@@ -4823,7 +4989,7 @@ var SDK_VERSION;
4823
4989
  var init_version = __esm({
4824
4990
  "src/version.ts"() {
4825
4991
  "use strict";
4826
- SDK_VERSION = "3.10.0";
4992
+ SDK_VERSION = "3.11.0";
4827
4993
  }
4828
4994
  });
4829
4995
 
@@ -5793,10 +5959,12 @@ function toGatewayConfig(config) {
5793
5959
  };
5794
5960
  }
5795
5961
  function toVerificationRequest(context, astraId) {
5962
+ const dotIndex = context.purpose.indexOf(".");
5963
+ const dotted = dotIndex > 0;
5796
5964
  return {
5797
5965
  credentials: { astraId },
5798
- purpose: context.purpose,
5799
- action: context.action,
5966
+ purpose: dotted ? context.purpose.slice(0, dotIndex) : context.purpose,
5967
+ action: dotted ? context.purpose : context.action,
5800
5968
  resourceType: context.resourceType,
5801
5969
  resource: context.target,
5802
5970
  transactionValue: context.transactionValue,
@@ -5839,21 +6007,93 @@ var init_online = __esm({
5839
6007
  }
5840
6008
  });
5841
6009
 
6010
+ // src/gateway/redact.ts
6011
+ function redactSecrets(text) {
6012
+ let result = text;
6013
+ for (const { type: type2, re, replacement } of REDACTION_PATTERNS) {
6014
+ result = result.replace(re, replacement ?? marker(type2));
6015
+ }
6016
+ return result;
6017
+ }
6018
+ function redactDeep(value) {
6019
+ if (typeof value === "string") {
6020
+ return redactSecrets(value);
6021
+ }
6022
+ if (Array.isArray(value)) {
6023
+ return value.map((item) => redactDeep(item));
6024
+ }
6025
+ if (value !== null && typeof value === "object" && !(value instanceof Date)) {
6026
+ const out = {};
6027
+ for (const [k, v] of Object.entries(value)) {
6028
+ out[k] = redactDeep(v);
6029
+ }
6030
+ return out;
6031
+ }
6032
+ return value;
6033
+ }
6034
+ var marker, REDACTION_PATTERNS;
6035
+ var init_redact = __esm({
6036
+ "src/gateway/redact.ts"() {
6037
+ "use strict";
6038
+ marker = (type2) => `[REDACTED:${type2}]`;
6039
+ REDACTION_PATTERNS = [
6040
+ // Multi-line blocks first so later single-line patterns don't shred them.
6041
+ {
6042
+ type: "private-key",
6043
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
6044
+ },
6045
+ // URL userinfo credentials (https://user:pass@, x-access-token:ghp_…@).
6046
+ {
6047
+ type: "url-credentials",
6048
+ re: /([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+:[^\s/@]+@/gi,
6049
+ replacement: `$1${marker("url-credentials")}@`
6050
+ },
6051
+ // Known token formats.
6052
+ { type: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{20,255}\b/g },
6053
+ { type: "github-token", re: /\bgithub_pat_[A-Za-z0-9_]{20,255}\b/g },
6054
+ { type: "astrasync-key", re: /\bkya_[A-Fa-f0-9]{32,128}\b/g },
6055
+ { type: "openai-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
6056
+ { type: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/g },
6057
+ { type: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
6058
+ // Bearer/token headers.
6059
+ {
6060
+ type: "bearer-token",
6061
+ re: /\b(bearer\s+)[a-z0-9._~+/=-]{16,}/gi,
6062
+ replacement: `$1${marker("bearer-token")}`
6063
+ },
6064
+ // Generic KEY=value / key: value assignments with secret-ish names.
6065
+ {
6066
+ type: "assigned-secret",
6067
+ re: /\b((?:password|passwd|secret|token|api[_-]?key|apikey|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)(['"]?)[^\s'"&;]{8,}\2/gi,
6068
+ replacement: `$1$2${marker("assigned-secret")}$2`
6069
+ }
6070
+ ];
6071
+ }
6072
+ });
6073
+
5842
6074
  // src/gateway/modes/hybrid.ts
6075
+ function delay(ms) {
6076
+ return new Promise((resolve) => {
6077
+ const timer = setTimeout(() => resolve(FLUSH_TIMEOUT), ms);
6078
+ if (typeof timer.unref === "function") timer.unref();
6079
+ });
6080
+ }
5843
6081
  function verifyHybrid(evaluator, syncQueue, context) {
5844
6082
  const decision = evaluator.evaluate(context);
5845
6083
  syncQueue.enqueue(context, decision);
5846
6084
  return decision;
5847
6085
  }
5848
- var import_fs2, import_path, MAX_QUEUE_SIZE, MAX_RETRIES, HybridSyncQueue;
6086
+ var import_fs2, import_path, MAX_QUEUE_SIZE, MAX_RETRIES, FLUSH_TIMEOUT, HybridSyncQueue;
5849
6087
  var init_hybrid = __esm({
5850
6088
  "src/gateway/modes/hybrid.ts"() {
5851
6089
  "use strict";
5852
6090
  import_fs2 = require("fs");
5853
6091
  import_path = require("path");
5854
6092
  init_online();
6093
+ init_redact();
5855
6094
  MAX_QUEUE_SIZE = 1e3;
5856
6095
  MAX_RETRIES = 3;
6096
+ FLUSH_TIMEOUT = /* @__PURE__ */ Symbol("flush-timeout");
5857
6097
  HybridSyncQueue = class {
5858
6098
  constructor(config, persistPath) {
5859
6099
  this.queue = [];
@@ -5874,12 +6114,12 @@ var init_hybrid = __esm({
5874
6114
  /**
5875
6115
  * Stop periodic sync and flush remaining entries.
5876
6116
  */
5877
- async stop() {
6117
+ async stop(flushOptions = {}) {
5878
6118
  if (this.timer) {
5879
6119
  clearInterval(this.timer);
5880
6120
  this.timer = null;
5881
6121
  }
5882
- await this.flush();
6122
+ await this.flush(flushOptions);
5883
6123
  this.saveToDisk();
5884
6124
  }
5885
6125
  /**
@@ -5891,8 +6131,8 @@ var init_hybrid = __esm({
5891
6131
  }
5892
6132
  this.queue.push({
5893
6133
  id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
5894
- context,
5895
- decision,
6134
+ context: redactDeep(context),
6135
+ decision: redactDeep(decision),
5896
6136
  timestamp: /* @__PURE__ */ new Date(),
5897
6137
  retryCount: 0,
5898
6138
  status: "pending"
@@ -5902,16 +6142,21 @@ var init_hybrid = __esm({
5902
6142
  /**
5903
6143
  * Flush pending entries to the API.
5904
6144
  */
5905
- async flush() {
6145
+ async flush(options = {}) {
5906
6146
  if (this.processing) return;
5907
6147
  this.processing = true;
5908
6148
  try {
5909
6149
  const pending = this.queue.filter((e) => e.status === "pending");
5910
- for (const entry of pending) {
5911
- try {
5912
- await verifyOnline(this.config, entry.context);
6150
+ const batch = typeof options.limit === "number" ? pending.slice(0, options.limit) : pending;
6151
+ for (const entry of batch) {
6152
+ const attempt = verifyOnline(this.config, entry.context, this.config.agentId).then(() => "synced").catch(() => "error");
6153
+ const result = options.perEntryTimeoutMs ? await Promise.race([attempt, delay(options.perEntryTimeoutMs)]) : await attempt;
6154
+ if (result === FLUSH_TIMEOUT) {
6155
+ break;
6156
+ }
6157
+ if (result === "synced") {
5913
6158
  entry.status = "synced";
5914
- } catch {
6159
+ } else {
5915
6160
  entry.retryCount++;
5916
6161
  if (entry.retryCount >= MAX_RETRIES) {
5917
6162
  entry.status = "failed";
@@ -5970,6 +6215,7 @@ var init_trace_logger = __esm({
5970
6215
  "use strict";
5971
6216
  import_fs3 = require("fs");
5972
6217
  import_path2 = require("path");
6218
+ init_redact();
5973
6219
  MAX_FILE_SIZE = 10 * 1024 * 1024;
5974
6220
  MAX_FILES = 10;
5975
6221
  TraceLogger = class {
@@ -5989,8 +6235,22 @@ var init_trace_logger = __esm({
5989
6235
  id: this.generateId(),
5990
6236
  timestamp: /* @__PURE__ */ new Date(),
5991
6237
  type: "evaluation",
5992
- context,
5993
- decision
6238
+ context: redactDeep(context),
6239
+ decision: redactDeep(decision)
6240
+ });
6241
+ }
6242
+ /**
6243
+ * Log a tool call the guard saw but did not govern. One line per skip
6244
+ * keeps coverage holes (unmapped platform tools) visible in the traces
6245
+ * instead of silently indistinguishable from ALLOW.
6246
+ */
6247
+ logSkipped(toolName, reason) {
6248
+ if (!this.enabled) return;
6249
+ this.write({
6250
+ id: this.generateId(),
6251
+ timestamp: /* @__PURE__ */ new Date(),
6252
+ type: "skipped",
6253
+ metadata: { toolName, reason }
5994
6254
  });
5995
6255
  }
5996
6256
  /**
@@ -6118,10 +6378,12 @@ var init_gateway = __esm({
6118
6378
  }
6119
6379
  /**
6120
6380
  * Graceful shutdown. Flushes sync queue and cleans up resources.
6381
+ * Stateless callers (hooks) pass bounded flush options so sync never
6382
+ * sits on a tool call's critical path.
6121
6383
  */
6122
- async shutdown() {
6384
+ async shutdown(options) {
6123
6385
  if (this.syncQueue) {
6124
- await this.syncQueue.stop();
6386
+ await this.syncQueue.stop(options?.syncFlush ?? {});
6125
6387
  this.syncQueue = null;
6126
6388
  }
6127
6389
  this.evaluator = null;
@@ -6144,7 +6406,7 @@ var init_gateway = __esm({
6144
6406
  let decision;
6145
6407
  switch (this.config.mode) {
6146
6408
  case "online":
6147
- decision = await verifyOnline(this.config, context, astraId);
6409
+ decision = await verifyOnline(this.config, context, astraId ?? this.config.agentId);
6148
6410
  break;
6149
6411
  case "local":
6150
6412
  if (!this.evaluator) throw new Error("Local evaluator not initialized");
@@ -6199,6 +6461,13 @@ var init_gateway = __esm({
6199
6461
  getSyncStatus() {
6200
6462
  return this.syncQueue?.getStatus() || null;
6201
6463
  }
6464
+ /**
6465
+ * Record a tool call the calling adapter saw but did not govern
6466
+ * (unmapped tool, skipTools, MCP with governMcp off). Trace-only.
6467
+ */
6468
+ logSkippedTool(toolName, reason) {
6469
+ this.traceLogger?.logSkipped(toolName, reason);
6470
+ }
6202
6471
  };
6203
6472
  }
6204
6473
  });
@@ -6333,9 +6602,9 @@ var init_git_hooks = __esm({
6333
6602
  });
6334
6603
 
6335
6604
  // src/claude-code/settings-installer.ts
6336
- function isGuardEntry(entry) {
6605
+ function isGuardEntry(entry, marker2) {
6337
6606
  return (entry.hooks ?? []).some(
6338
- (h) => typeof h.command === "string" && h.command.includes(ASTRASYNC_HOOK_MARKER)
6607
+ (h) => typeof h.command === "string" && h.command.includes(marker2)
6339
6608
  );
6340
6609
  }
6341
6610
  function readSettings(settingsPath) {
@@ -6349,24 +6618,26 @@ function writeSettings(settingsPath, settings) {
6349
6618
  (0, import_fs5.writeFileSync)(settingsPath, `${JSON.stringify(settings, null, 2)}
6350
6619
  `, "utf-8");
6351
6620
  }
6352
- function buildHookEntry(hookCommand) {
6621
+ function buildHookEntry(hookCommand, matcher = HOOK_MATCHER, extraHookFields = {}) {
6353
6622
  return {
6354
- matcher: HOOK_MATCHER,
6355
- hooks: [{ type: "command", command: hookCommand, timeout: HOOK_TIMEOUT_SECONDS }]
6623
+ matcher,
6624
+ hooks: [
6625
+ { type: "command", command: hookCommand, timeout: HOOK_TIMEOUT_SECONDS, ...extraHookFields }
6626
+ ]
6356
6627
  };
6357
6628
  }
6358
- function installClaudeHooks(settingsPath, hookCommand) {
6359
- if (!hookCommand.includes(ASTRASYNC_HOOK_MARKER)) {
6629
+ function installClaudeHooks(settingsPath, hookCommand, marker2 = ASTRASYNC_HOOK_MARKER, matcher = HOOK_MATCHER, extraHookFields = {}) {
6630
+ if (!hookCommand.includes(marker2)) {
6360
6631
  throw new Error(
6361
- `Hook command must reference ${ASTRASYNC_HOOK_MARKER} (got: ${hookCommand}) \u2014 the marker is how install/uninstall identify the guard entry.`
6632
+ `Hook command must reference ${marker2} (got: ${hookCommand}) \u2014 the marker is how install/uninstall identify the guard entry.`
6362
6633
  );
6363
6634
  }
6364
6635
  const settings = readSettings(settingsPath);
6365
6636
  settings.hooks = settings.hooks ?? {};
6366
6637
  const preToolUse = settings.hooks.PreToolUse = settings.hooks.PreToolUse ?? [];
6367
- const existing = preToolUse.find(isGuardEntry);
6638
+ const existing = preToolUse.find((entry) => isGuardEntry(entry, marker2));
6368
6639
  if (existing) {
6369
- const fresh = buildHookEntry(hookCommand);
6640
+ const fresh = buildHookEntry(hookCommand, matcher, extraHookFields);
6370
6641
  const changed = JSON.stringify(existing) !== JSON.stringify(fresh);
6371
6642
  if (changed) {
6372
6643
  preToolUse[preToolUse.indexOf(existing)] = fresh;
@@ -6374,11 +6645,11 @@ function installClaudeHooks(settingsPath, hookCommand) {
6374
6645
  }
6375
6646
  return { installed: false, alreadyPresent: true, updated: changed };
6376
6647
  }
6377
- preToolUse.push(buildHookEntry(hookCommand));
6648
+ preToolUse.push(buildHookEntry(hookCommand, matcher, extraHookFields));
6378
6649
  writeSettings(settingsPath, settings);
6379
6650
  return { installed: true, alreadyPresent: false, updated: false };
6380
6651
  }
6381
- function uninstallClaudeHooks(settingsPath) {
6652
+ function uninstallClaudeHooks(settingsPath, marker2 = ASTRASYNC_HOOK_MARKER) {
6382
6653
  if (!(0, import_fs5.existsSync)(settingsPath)) return false;
6383
6654
  let settings;
6384
6655
  try {
@@ -6388,7 +6659,7 @@ function uninstallClaudeHooks(settingsPath) {
6388
6659
  }
6389
6660
  const preToolUse = settings.hooks?.PreToolUse;
6390
6661
  if (!preToolUse) return false;
6391
- const remaining = preToolUse.filter((entry) => !isGuardEntry(entry));
6662
+ const remaining = preToolUse.filter((entry) => !isGuardEntry(entry, marker2));
6392
6663
  if (remaining.length === preToolUse.length) return false;
6393
6664
  if (remaining.length > 0) {
6394
6665
  settings.hooks.PreToolUse = remaining;
@@ -6452,16 +6723,44 @@ var init_settings_installer = __esm({
6452
6723
  init_wizard();
6453
6724
  init_pdlss_parser();
6454
6725
  ASTRASYNC_HOOK_MARKER = "astrasync-claude-hook";
6455
- HOOK_MATCHER = "Bash|Write|Edit|MultiEdit|NotebookEdit|Read|Glob|Grep|WebFetch|WebSearch|Task|Agent|mcp__.*";
6726
+ HOOK_MATCHER = "Bash|PowerShell|Write|Edit|MultiEdit|NotebookEdit|Read|Glob|Grep|WebFetch|WebSearch|Task|Agent|mcp__.*";
6456
6727
  HOOK_TIMEOUT_SECONDS = 10;
6457
6728
  GUARD_SELF_PROTECTION_PATTERNS = [
6458
6729
  "**/.claude/settings.json",
6459
6730
  "**/.claude/settings.local.json",
6460
- "**/.astrasync/**"
6731
+ "**/.astrasync/**",
6732
+ "**/.codex/hooks.json",
6733
+ "**/.codex/config.toml"
6461
6734
  ];
6462
6735
  }
6463
6736
  });
6464
6737
 
6738
+ // src/codex/settings-installer.ts
6739
+ function installCodexHooks(hooksPath, hookCommand) {
6740
+ return installClaudeHooks(
6741
+ hooksPath,
6742
+ hookCommand,
6743
+ ASTRASYNC_CODEX_HOOK_MARKER,
6744
+ CODEX_HOOK_MATCHER,
6745
+ {
6746
+ statusMessage: CODEX_STATUS_MESSAGE
6747
+ }
6748
+ );
6749
+ }
6750
+ function uninstallCodexHooks(hooksPath) {
6751
+ return uninstallClaudeHooks(hooksPath, ASTRASYNC_CODEX_HOOK_MARKER);
6752
+ }
6753
+ var ASTRASYNC_CODEX_HOOK_MARKER, CODEX_HOOK_MATCHER, CODEX_STATUS_MESSAGE;
6754
+ var init_settings_installer2 = __esm({
6755
+ "src/codex/settings-installer.ts"() {
6756
+ "use strict";
6757
+ init_settings_installer();
6758
+ ASTRASYNC_CODEX_HOOK_MARKER = "astrasync-codex-hook";
6759
+ CODEX_HOOK_MATCHER = "Bash|apply_patch|Edit|Write|web_search|mcp__.*";
6760
+ CODEX_STATUS_MESSAGE = "AstraSync Local Guard";
6761
+ }
6762
+ });
6763
+
6465
6764
  // src/cli/guard.ts
6466
6765
  var guard_exports = {};
6467
6766
  function createPrompt() {
@@ -6626,6 +6925,7 @@ async function cmdStatus() {
6626
6925
  console.log(' Policy: not found \u2014 run "astrasync guard setup"');
6627
6926
  }
6628
6927
  printClaudeCodeStatus();
6928
+ printCodexStatus();
6629
6929
  console.log("");
6630
6930
  }
6631
6931
  async function cmdCheck() {
@@ -6712,7 +7012,7 @@ async function cmdUninstallHooks() {
6712
7012
  function resolveClaudeHookCommand() {
6713
7013
  const sibling = path.join(__dirname, "astrasync-claude-hook.js");
6714
7014
  if (fs.existsSync(sibling)) {
6715
- return `node "${sibling}"`;
7015
+ return `"${process.execPath}" "${sibling}"`;
6716
7016
  }
6717
7017
  return "astrasync-claude-hook";
6718
7018
  }
@@ -6804,6 +7104,108 @@ async function cmdUninstallClaudeCode() {
6804
7104
  `[AstraSync] Policy and traces left in place at ${astrasyncDir} \u2014 delete manually if unwanted.`
6805
7105
  );
6806
7106
  }
7107
+ function resolveCodexHookCommand() {
7108
+ const sibling = path.join(__dirname, "astrasync-codex-hook.js");
7109
+ if (fs.existsSync(sibling)) {
7110
+ return `"${process.execPath}" "${sibling}"`;
7111
+ }
7112
+ return "astrasync-codex-hook";
7113
+ }
7114
+ function codexPaths(project) {
7115
+ const root = project ? process.cwd() : os.homedir();
7116
+ return {
7117
+ hooksPath: path.join(root, ".codex", "hooks.json"),
7118
+ astrasyncDir: path.join(root, ".astrasync")
7119
+ };
7120
+ }
7121
+ async function cmdInstallCodex() {
7122
+ const flags = parseClaudeCodeFlags();
7123
+ const { hooksPath, astrasyncDir } = codexPaths(flags.project);
7124
+ const policyPath = flags.policyOverride || path.join(astrasyncDir, "policy.yaml");
7125
+ const guardJsonPath = path.join(astrasyncDir, "guard.json");
7126
+ console.log("\nAstraSync Local Guard for Codex CLI \u2014 install\n");
7127
+ if (fs.existsSync(policyPath)) {
7128
+ console.log(` Policy: using existing ${policyPath} (shared across adapters)`);
7129
+ if (ensureGuardSelfProtection(policyPath)) {
7130
+ console.log(" added guard self-protection patterns to file.write");
7131
+ }
7132
+ } else if (flags.wizard) {
7133
+ const prompt = createPrompt();
7134
+ try {
7135
+ fs.mkdirSync(path.dirname(policyPath), { recursive: true });
7136
+ const wizard = new SetupWizard(prompt);
7137
+ await wizard.runAndSave(policyPath);
7138
+ } finally {
7139
+ prompt.close();
7140
+ }
7141
+ ensureGuardSelfProtection(policyPath);
7142
+ console.log(` Policy: created via wizard at ${policyPath}`);
7143
+ } else {
7144
+ fs.mkdirSync(path.dirname(policyPath), { recursive: true });
7145
+ fs.writeFileSync(policyPath, toYaml(buildDefaultClaudeCodePolicy()), "utf-8");
7146
+ console.log(` Policy: default coding-agent policy written to ${policyPath}`);
7147
+ console.log(" (re-run with --wizard for the interactive setup instead)");
7148
+ }
7149
+ if (!fs.existsSync(guardJsonPath)) {
7150
+ const guardSettings = {
7151
+ posture: flags.active ? "active" : "passive",
7152
+ governMcp: false
7153
+ };
7154
+ fs.writeFileSync(guardJsonPath, `${JSON.stringify(guardSettings, null, 2)}
7155
+ `, "utf-8");
7156
+ console.log(
7157
+ ` Guard: ${guardJsonPath} (posture: ${guardSettings.posture}, governMcp: false)`
7158
+ );
7159
+ } else {
7160
+ console.log(` Guard: using existing ${guardJsonPath}`);
7161
+ }
7162
+ const hookCommand = resolveCodexHookCommand();
7163
+ const result = installCodexHooks(hooksPath, hookCommand);
7164
+ if (result.installed) {
7165
+ console.log(` Hook: installed in ${hooksPath}`);
7166
+ } else if (result.updated) {
7167
+ console.log(` Hook: updated in ${hooksPath}`);
7168
+ } else {
7169
+ console.log(` Hook: already present in ${hooksPath}`);
7170
+ }
7171
+ console.log("\n Next steps:");
7172
+ console.log(" 1. Start a NEW Codex session (hooks load at startup; /hooks to inspect).");
7173
+ console.log(` 2. Watch decisions: tail -f ${path.join(astrasyncDir, "traces")}/*.jsonl`);
7174
+ console.log(` 3. When the traces look right, set "posture": "active" in ${guardJsonPath}.`);
7175
+ console.log(" Denies block; approvals fall through to Codex's own prompt \u2014");
7176
+ console.log(" except in dontAsk/bypassPermissions modes, where they deny.");
7177
+ console.log(" 4. Note: Codex fails OPEN if the hook itself times out; the guard");
7178
+ console.log(" fails closed on every failure it can see.\n");
7179
+ }
7180
+ async function cmdUninstallCodex() {
7181
+ const flags = parseClaudeCodeFlags();
7182
+ const { hooksPath, astrasyncDir } = codexPaths(flags.project);
7183
+ const removed = uninstallCodexHooks(hooksPath);
7184
+ if (removed) {
7185
+ console.log(`[AstraSync] Removed Codex hook from ${hooksPath}`);
7186
+ } else {
7187
+ console.log(`[AstraSync] No AstraSync hook found in ${hooksPath}`);
7188
+ }
7189
+ console.log(
7190
+ `[AstraSync] Policy and traces left in place at ${astrasyncDir} \u2014 delete manually if unwanted.`
7191
+ );
7192
+ }
7193
+ function printCodexStatus() {
7194
+ for (const scope of ["user", "project"]) {
7195
+ const { hooksPath, astrasyncDir } = codexPaths(scope === "project");
7196
+ let hookInstalled = false;
7197
+ try {
7198
+ const content = fs.readFileSync(hooksPath, "utf-8");
7199
+ hookInstalled = content.includes(ASTRASYNC_CODEX_HOOK_MARKER);
7200
+ } catch {
7201
+ }
7202
+ if (hookInstalled) {
7203
+ const policyPath = path.join(astrasyncDir, "policy.yaml");
7204
+ const policy = fs.existsSync(policyPath) ? `policy: ${policyPath}` : "policy: missing";
7205
+ console.log(` Codex CLI (${scope}): hook installed, ${policy}`);
7206
+ }
7207
+ }
7208
+ }
6807
7209
  function printClaudeCodeStatus() {
6808
7210
  for (const scope of ["user", "project"]) {
6809
7211
  const { settingsPath, astrasyncDir } = claudeCodePaths(scope === "project");
@@ -6861,6 +7263,12 @@ async function main() {
6861
7263
  case "uninstall-claude-code":
6862
7264
  await cmdUninstallClaudeCode();
6863
7265
  break;
7266
+ case "install-codex":
7267
+ await cmdInstallCodex();
7268
+ break;
7269
+ case "uninstall-codex":
7270
+ await cmdUninstallCodex();
7271
+ break;
6864
7272
  default:
6865
7273
  console.log("AstraSync Local Guard\n");
6866
7274
  console.log("Usage: astrasync guard <command>\n");
@@ -6878,6 +7286,9 @@ async function main() {
6878
7286
  console.log(
6879
7287
  " uninstall-claude-code Remove the Claude Code hook (--project for project scope)"
6880
7288
  );
7289
+ console.log(" install-codex Install the Local Guard hook for OpenAI Codex CLI");
7290
+ console.log(" (same flags; policy shared with Claude Code)");
7291
+ console.log(" uninstall-codex Remove the Codex hook (--project for project scope)");
6881
7292
  console.log("");
6882
7293
  process.exit(command ? 1 : 0);
6883
7294
  }
@@ -6897,6 +7308,7 @@ var init_guard = __esm({
6897
7308
  init_pdlss_parser();
6898
7309
  init_git_hooks();
6899
7310
  init_settings_installer();
7311
+ init_settings_installer2();
6900
7312
  DEFAULT_POLICY_PATH = "./local-policy.yaml";
6901
7313
  DEFAULT_CONFIG_DIR = ".astrasync";
6902
7314
  main().catch((err) => {