@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
@@ -4185,7 +4185,9 @@ var HIGH_RISK_COMMANDS = [
4185
4185
  "git push",
4186
4186
  "npm publish",
4187
4187
  "docker",
4188
- "kubectl"
4188
+ "kubectl",
4189
+ "gh pr create",
4190
+ "gh pr merge"
4189
4191
  ];
4190
4192
  var SENSITIVE_PATHS = [
4191
4193
  ".ssh",
@@ -4204,6 +4206,164 @@ var SENSITIVE_PATHS = [
4204
4206
  ".pypirc"
4205
4207
  ];
4206
4208
 
4209
+ // src/local-evaluator/command-matcher.ts
4210
+ var WRAPPERS = /* @__PURE__ */ new Set([
4211
+ "sudo",
4212
+ "doas",
4213
+ "env",
4214
+ "nohup",
4215
+ "time",
4216
+ "timeout",
4217
+ "nice",
4218
+ "ionice",
4219
+ "stdbuf",
4220
+ "xargs",
4221
+ "command",
4222
+ "exec",
4223
+ "builtin"
4224
+ ]);
4225
+ var ASSIGN_RE = /^[a-z_][a-z0-9_]*=/;
4226
+ var MAX_WRAPPER_HOPS = 4;
4227
+ var MAX_RECURSION_DEPTH = 3;
4228
+ var MAX_COMMAND_LENGTH = 1e5;
4229
+ var LOCAL_HOST_RE = /^(localhost|127\.0\.0\.1|::1|[a-z0-9.-]*\.localhost)$/;
4230
+ function buildRiskList(entries) {
4231
+ const singleWord = /* @__PURE__ */ new Set();
4232
+ const multiWord = [];
4233
+ for (const entry of entries) {
4234
+ const words = entry.toLowerCase().trim().split(/\s+/);
4235
+ if (words.length === 1) singleWord.add(words[0]);
4236
+ else multiWord.push(words);
4237
+ }
4238
+ multiWord.sort((a, b) => b.length - a.length);
4239
+ return { singleWord, multiWord };
4240
+ }
4241
+ function stripHeredocBodies(text) {
4242
+ const lines = text.split("\n");
4243
+ const kept = [];
4244
+ let terminator = null;
4245
+ for (const line of lines) {
4246
+ if (terminator !== null) {
4247
+ if (line.trim() === terminator) terminator = null;
4248
+ continue;
4249
+ }
4250
+ kept.push(line);
4251
+ const heredoc = line.match(/<<-?\s*['"]?(\w+)['"]?/);
4252
+ if (heredoc) terminator = heredoc[1];
4253
+ }
4254
+ return kept.join("\n");
4255
+ }
4256
+ function extractDollarSubstitutions(text, collect) {
4257
+ let result = text;
4258
+ let start = result.indexOf("$(");
4259
+ let iterations = 0;
4260
+ while (start !== -1 && iterations < 50) {
4261
+ iterations++;
4262
+ let depth = 0;
4263
+ let end = result.length;
4264
+ for (let i = start + 1; i < result.length; i++) {
4265
+ if (result[i] === "(") depth++;
4266
+ else if (result[i] === ")") {
4267
+ depth--;
4268
+ if (depth === 0) {
4269
+ end = i;
4270
+ break;
4271
+ }
4272
+ }
4273
+ }
4274
+ collect(result.slice(start + 2, end));
4275
+ result = `${result.slice(0, start)} ${result.slice(Math.min(end + 1, result.length))}`;
4276
+ start = result.indexOf("$(");
4277
+ }
4278
+ return result;
4279
+ }
4280
+ function extractBacktickSubstitutions(text, collect) {
4281
+ if (!text.includes("`")) return text;
4282
+ const chunks = text.split("`");
4283
+ const kept = [];
4284
+ for (let i = 0; i < chunks.length; i++) {
4285
+ if (i % 2 === 1) collect(chunks[i]);
4286
+ else kept.push(chunks[i]);
4287
+ }
4288
+ return kept.join(" ");
4289
+ }
4290
+ function basename(token) {
4291
+ const slash = token.lastIndexOf("/");
4292
+ return slash === -1 ? token : token.slice(slash + 1);
4293
+ }
4294
+ function matchSegment(segment, list) {
4295
+ const tokens = segment.split(/\s+/).filter((t) => /[a-z0-9]/.test(t));
4296
+ let i = 0;
4297
+ let hops = 0;
4298
+ while (i < tokens.length) {
4299
+ while (i < tokens.length && (tokens[i] === "export" || ASSIGN_RE.test(tokens[i]))) i++;
4300
+ if (i >= tokens.length) return null;
4301
+ const cmd = basename(tokens[i]);
4302
+ const significant = [cmd, ...tokens.slice(i + 1).filter((t) => !t.startsWith("-"))];
4303
+ for (const entry of list.multiWord) {
4304
+ if (entry.length <= significant.length && entry.every((w, j) => significant[j] === w)) {
4305
+ return entry.join(" ");
4306
+ }
4307
+ }
4308
+ if (list.singleWord.has(cmd)) return cmd;
4309
+ if (WRAPPERS.has(cmd) && hops < MAX_WRAPPER_HOPS) {
4310
+ hops++;
4311
+ i++;
4312
+ while (i < tokens.length && (tokens[i].startsWith("-") || ASSIGN_RE.test(tokens[i]))) i++;
4313
+ continue;
4314
+ }
4315
+ return null;
4316
+ }
4317
+ return null;
4318
+ }
4319
+ function collectMatches(command, list, matches, depth) {
4320
+ if (depth > MAX_RECURSION_DEPTH || command.length > MAX_COMMAND_LENGTH) return;
4321
+ let text = command.toLowerCase();
4322
+ text = stripHeredocBodies(text);
4323
+ const recurse = (inner) => collectMatches(inner, list, matches, depth + 1);
4324
+ text = extractDollarSubstitutions(text, recurse);
4325
+ text = extractBacktickSubstitutions(text, recurse);
4326
+ text = text.replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
4327
+ text = text.replace(/(^|\s)#[^\n]*/g, " ");
4328
+ const segments = text.split(/\|\||&&|[;|&\n]|[({]/);
4329
+ for (const segment of segments) {
4330
+ const matched = matchSegment(segment, list);
4331
+ if (matched) matches.add(matched);
4332
+ }
4333
+ }
4334
+ function matchHighRiskCommands(command, highRiskCommands = HIGH_RISK_COMMANDS) {
4335
+ if (!command) return [];
4336
+ const matches = /* @__PURE__ */ new Set();
4337
+ collectMatches(command, buildRiskList(highRiskCommands), matches, 0);
4338
+ return [...matches];
4339
+ }
4340
+ function isLocalhostOnlyNetworkCommand(command) {
4341
+ const text = command.toLowerCase();
4342
+ const tokens = text.split(/\s+/).map((t) => t.replace(/^["'`]+|["'`,;)]+$/g, ""));
4343
+ let localCount = 0;
4344
+ for (const token of tokens) {
4345
+ let host = null;
4346
+ if (token.includes("://")) {
4347
+ let rest = token.slice(token.indexOf("://") + 3);
4348
+ const at = rest.indexOf("@");
4349
+ if (at !== -1) rest = rest.slice(at + 1);
4350
+ host = rest.split(/[/?#]/)[0];
4351
+ } else if (/^[a-z0-9.[\]:-]+:\d+(\/.*)?$/.test(token)) {
4352
+ host = token.split("/")[0];
4353
+ } else if (/^(localhost|127\.0\.0\.1|\[?::1\]?)(\/.*)?$/.test(token)) {
4354
+ host = token.split("/")[0];
4355
+ }
4356
+ if (host === null) continue;
4357
+ const bare = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
4358
+ if (LOCAL_HOST_RE.test(bare)) {
4359
+ localCount++;
4360
+ } else {
4361
+ return false;
4362
+ }
4363
+ }
4364
+ return localCount > 0;
4365
+ }
4366
+
4207
4367
  // src/local-evaluator/evaluator.ts
4208
4368
  var LocalEvaluator = class {
4209
4369
  constructor(policy) {
@@ -4354,12 +4514,10 @@ var LocalEvaluator = class {
4354
4514
  return score;
4355
4515
  }
4356
4516
  if (context.purpose === "shell.exec" && context.target) {
4357
- const targetLower = context.target.toLowerCase();
4358
- for (const cmd of HIGH_RISK_COMMANDS) {
4359
- if (targetLower.startsWith(cmd) || targetLower.includes(` ${cmd} `) || targetLower.includes(` ${cmd}`)) {
4360
- score = Math.max(score, 80);
4361
- break;
4362
- }
4517
+ const matched = matchHighRiskCommands(context.target);
4518
+ const localProbeOnly = matched.length > 0 && matched.every((cmd) => cmd === "curl" || cmd === "wget") && isLocalhostOnlyNetworkCommand(context.target);
4519
+ if (matched.length > 0 && !localProbeOnly) {
4520
+ score = Math.max(score, 80);
4363
4521
  }
4364
4522
  }
4365
4523
  if ((context.purpose === "file.read" || context.purpose === "file.write" || context.purpose === "file.delete") && context.target) {
@@ -4440,7 +4598,7 @@ function getTrustLevel(score) {
4440
4598
  }
4441
4599
 
4442
4600
  // src/version.ts
4443
- var SDK_VERSION = "3.10.0";
4601
+ var SDK_VERSION = "3.11.0";
4444
4602
 
4445
4603
  // src/well-known.ts
4446
4604
  var CACHE_TTL_MS = 60 * 60 * 1e3;
@@ -4955,10 +5113,12 @@ function toGatewayConfig(config) {
4955
5113
  };
4956
5114
  }
4957
5115
  function toVerificationRequest(context, astraId) {
5116
+ const dotIndex = context.purpose.indexOf(".");
5117
+ const dotted = dotIndex > 0;
4958
5118
  return {
4959
5119
  credentials: { astraId },
4960
- purpose: context.purpose,
4961
- action: context.action,
5120
+ purpose: dotted ? context.purpose.slice(0, dotIndex) : context.purpose,
5121
+ action: dotted ? context.purpose : context.action,
4962
5122
  resourceType: context.resourceType,
4963
5123
  resource: context.target,
4964
5124
  transactionValue: context.transactionValue,
@@ -4998,8 +5158,75 @@ async function verifyOnline(config, context, astraId) {
4998
5158
  // src/gateway/modes/hybrid.ts
4999
5159
  var import_fs2 = require("fs");
5000
5160
  var import_path = require("path");
5161
+
5162
+ // src/gateway/redact.ts
5163
+ var marker = (type2) => `[REDACTED:${type2}]`;
5164
+ var REDACTION_PATTERNS = [
5165
+ // Multi-line blocks first so later single-line patterns don't shred them.
5166
+ {
5167
+ type: "private-key",
5168
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
5169
+ },
5170
+ // URL userinfo credentials (https://user:pass@, x-access-token:ghp_…@).
5171
+ {
5172
+ type: "url-credentials",
5173
+ re: /([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+:[^\s/@]+@/gi,
5174
+ replacement: `$1${marker("url-credentials")}@`
5175
+ },
5176
+ // Known token formats.
5177
+ { type: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{20,255}\b/g },
5178
+ { type: "github-token", re: /\bgithub_pat_[A-Za-z0-9_]{20,255}\b/g },
5179
+ { type: "astrasync-key", re: /\bkya_[A-Fa-f0-9]{32,128}\b/g },
5180
+ { type: "openai-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
5181
+ { type: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/g },
5182
+ { type: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
5183
+ // Bearer/token headers.
5184
+ {
5185
+ type: "bearer-token",
5186
+ re: /\b(bearer\s+)[a-z0-9._~+/=-]{16,}/gi,
5187
+ replacement: `$1${marker("bearer-token")}`
5188
+ },
5189
+ // Generic KEY=value / key: value assignments with secret-ish names.
5190
+ {
5191
+ type: "assigned-secret",
5192
+ re: /\b((?:password|passwd|secret|token|api[_-]?key|apikey|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)(['"]?)[^\s'"&;]{8,}\2/gi,
5193
+ replacement: `$1$2${marker("assigned-secret")}$2`
5194
+ }
5195
+ ];
5196
+ function redactSecrets(text) {
5197
+ let result = text;
5198
+ for (const { type: type2, re, replacement } of REDACTION_PATTERNS) {
5199
+ result = result.replace(re, replacement ?? marker(type2));
5200
+ }
5201
+ return result;
5202
+ }
5203
+ function redactDeep(value) {
5204
+ if (typeof value === "string") {
5205
+ return redactSecrets(value);
5206
+ }
5207
+ if (Array.isArray(value)) {
5208
+ return value.map((item) => redactDeep(item));
5209
+ }
5210
+ if (value !== null && typeof value === "object" && !(value instanceof Date)) {
5211
+ const out = {};
5212
+ for (const [k, v] of Object.entries(value)) {
5213
+ out[k] = redactDeep(v);
5214
+ }
5215
+ return out;
5216
+ }
5217
+ return value;
5218
+ }
5219
+
5220
+ // src/gateway/modes/hybrid.ts
5001
5221
  var MAX_QUEUE_SIZE = 1e3;
5002
5222
  var MAX_RETRIES = 3;
5223
+ var FLUSH_TIMEOUT = /* @__PURE__ */ Symbol("flush-timeout");
5224
+ function delay(ms) {
5225
+ return new Promise((resolve) => {
5226
+ const timer = setTimeout(() => resolve(FLUSH_TIMEOUT), ms);
5227
+ if (typeof timer.unref === "function") timer.unref();
5228
+ });
5229
+ }
5003
5230
  var HybridSyncQueue = class {
5004
5231
  constructor(config, persistPath) {
5005
5232
  this.queue = [];
@@ -5020,12 +5247,12 @@ var HybridSyncQueue = class {
5020
5247
  /**
5021
5248
  * Stop periodic sync and flush remaining entries.
5022
5249
  */
5023
- async stop() {
5250
+ async stop(flushOptions = {}) {
5024
5251
  if (this.timer) {
5025
5252
  clearInterval(this.timer);
5026
5253
  this.timer = null;
5027
5254
  }
5028
- await this.flush();
5255
+ await this.flush(flushOptions);
5029
5256
  this.saveToDisk();
5030
5257
  }
5031
5258
  /**
@@ -5037,8 +5264,8 @@ var HybridSyncQueue = class {
5037
5264
  }
5038
5265
  this.queue.push({
5039
5266
  id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
5040
- context,
5041
- decision,
5267
+ context: redactDeep(context),
5268
+ decision: redactDeep(decision),
5042
5269
  timestamp: /* @__PURE__ */ new Date(),
5043
5270
  retryCount: 0,
5044
5271
  status: "pending"
@@ -5048,16 +5275,21 @@ var HybridSyncQueue = class {
5048
5275
  /**
5049
5276
  * Flush pending entries to the API.
5050
5277
  */
5051
- async flush() {
5278
+ async flush(options = {}) {
5052
5279
  if (this.processing) return;
5053
5280
  this.processing = true;
5054
5281
  try {
5055
5282
  const pending = this.queue.filter((e) => e.status === "pending");
5056
- for (const entry of pending) {
5057
- try {
5058
- await verifyOnline(this.config, entry.context);
5283
+ const batch = typeof options.limit === "number" ? pending.slice(0, options.limit) : pending;
5284
+ for (const entry of batch) {
5285
+ const attempt = verifyOnline(this.config, entry.context, this.config.agentId).then(() => "synced").catch(() => "error");
5286
+ const result = options.perEntryTimeoutMs ? await Promise.race([attempt, delay(options.perEntryTimeoutMs)]) : await attempt;
5287
+ if (result === FLUSH_TIMEOUT) {
5288
+ break;
5289
+ }
5290
+ if (result === "synced") {
5059
5291
  entry.status = "synced";
5060
- } catch {
5292
+ } else {
5061
5293
  entry.retryCount++;
5062
5294
  if (entry.retryCount >= MAX_RETRIES) {
5063
5295
  entry.status = "failed";
@@ -5134,8 +5366,22 @@ var TraceLogger = class {
5134
5366
  id: this.generateId(),
5135
5367
  timestamp: /* @__PURE__ */ new Date(),
5136
5368
  type: "evaluation",
5137
- context,
5138
- decision
5369
+ context: redactDeep(context),
5370
+ decision: redactDeep(decision)
5371
+ });
5372
+ }
5373
+ /**
5374
+ * Log a tool call the guard saw but did not govern. One line per skip
5375
+ * keeps coverage holes (unmapped platform tools) visible in the traces
5376
+ * instead of silently indistinguishable from ALLOW.
5377
+ */
5378
+ logSkipped(toolName, reason) {
5379
+ if (!this.enabled) return;
5380
+ this.write({
5381
+ id: this.generateId(),
5382
+ timestamp: /* @__PURE__ */ new Date(),
5383
+ type: "skipped",
5384
+ metadata: { toolName, reason }
5139
5385
  });
5140
5386
  }
5141
5387
  /**
@@ -5252,10 +5498,12 @@ var AstraSyncGateway = class {
5252
5498
  }
5253
5499
  /**
5254
5500
  * Graceful shutdown. Flushes sync queue and cleans up resources.
5501
+ * Stateless callers (hooks) pass bounded flush options so sync never
5502
+ * sits on a tool call's critical path.
5255
5503
  */
5256
- async shutdown() {
5504
+ async shutdown(options) {
5257
5505
  if (this.syncQueue) {
5258
- await this.syncQueue.stop();
5506
+ await this.syncQueue.stop(options?.syncFlush ?? {});
5259
5507
  this.syncQueue = null;
5260
5508
  }
5261
5509
  this.evaluator = null;
@@ -5278,7 +5526,7 @@ var AstraSyncGateway = class {
5278
5526
  let decision;
5279
5527
  switch (this.config.mode) {
5280
5528
  case "online":
5281
- decision = await verifyOnline(this.config, context, astraId);
5529
+ decision = await verifyOnline(this.config, context, astraId ?? this.config.agentId);
5282
5530
  break;
5283
5531
  case "local":
5284
5532
  if (!this.evaluator) throw new Error("Local evaluator not initialized");
@@ -5333,6 +5581,13 @@ var AstraSyncGateway = class {
5333
5581
  getSyncStatus() {
5334
5582
  return this.syncQueue?.getStatus() || null;
5335
5583
  }
5584
+ /**
5585
+ * Record a tool call the calling adapter saw but did not govern
5586
+ * (unmapped tool, skipTools, MCP with governMcp off). Trace-only.
5587
+ */
5588
+ logSkippedTool(toolName, reason) {
5589
+ this.traceLogger?.logSkipped(toolName, reason);
5590
+ }
5336
5591
  };
5337
5592
 
5338
5593
  // src/git-trigger/git-hooks.ts
@@ -5461,11 +5716,11 @@ async function evaluateChangedFiles(gateway, files, config = {}) {
5461
5716
  var import_fs5 = require("fs");
5462
5717
  var import_path4 = require("path");
5463
5718
  var ASTRASYNC_HOOK_MARKER = "astrasync-claude-hook";
5464
- var HOOK_MATCHER = "Bash|Write|Edit|MultiEdit|NotebookEdit|Read|Glob|Grep|WebFetch|WebSearch|Task|Agent|mcp__.*";
5719
+ var HOOK_MATCHER = "Bash|PowerShell|Write|Edit|MultiEdit|NotebookEdit|Read|Glob|Grep|WebFetch|WebSearch|Task|Agent|mcp__.*";
5465
5720
  var HOOK_TIMEOUT_SECONDS = 10;
5466
- function isGuardEntry(entry) {
5721
+ function isGuardEntry(entry, marker2) {
5467
5722
  return (entry.hooks ?? []).some(
5468
- (h) => typeof h.command === "string" && h.command.includes(ASTRASYNC_HOOK_MARKER)
5723
+ (h) => typeof h.command === "string" && h.command.includes(marker2)
5469
5724
  );
5470
5725
  }
5471
5726
  function readSettings(settingsPath) {
@@ -5479,24 +5734,26 @@ function writeSettings(settingsPath, settings) {
5479
5734
  (0, import_fs5.writeFileSync)(settingsPath, `${JSON.stringify(settings, null, 2)}
5480
5735
  `, "utf-8");
5481
5736
  }
5482
- function buildHookEntry(hookCommand) {
5737
+ function buildHookEntry(hookCommand, matcher = HOOK_MATCHER, extraHookFields = {}) {
5483
5738
  return {
5484
- matcher: HOOK_MATCHER,
5485
- hooks: [{ type: "command", command: hookCommand, timeout: HOOK_TIMEOUT_SECONDS }]
5739
+ matcher,
5740
+ hooks: [
5741
+ { type: "command", command: hookCommand, timeout: HOOK_TIMEOUT_SECONDS, ...extraHookFields }
5742
+ ]
5486
5743
  };
5487
5744
  }
5488
- function installClaudeHooks(settingsPath, hookCommand) {
5489
- if (!hookCommand.includes(ASTRASYNC_HOOK_MARKER)) {
5745
+ function installClaudeHooks(settingsPath, hookCommand, marker2 = ASTRASYNC_HOOK_MARKER, matcher = HOOK_MATCHER, extraHookFields = {}) {
5746
+ if (!hookCommand.includes(marker2)) {
5490
5747
  throw new Error(
5491
- `Hook command must reference ${ASTRASYNC_HOOK_MARKER} (got: ${hookCommand}) \u2014 the marker is how install/uninstall identify the guard entry.`
5748
+ `Hook command must reference ${marker2} (got: ${hookCommand}) \u2014 the marker is how install/uninstall identify the guard entry.`
5492
5749
  );
5493
5750
  }
5494
5751
  const settings = readSettings(settingsPath);
5495
5752
  settings.hooks = settings.hooks ?? {};
5496
5753
  const preToolUse = settings.hooks.PreToolUse = settings.hooks.PreToolUse ?? [];
5497
- const existing = preToolUse.find(isGuardEntry);
5754
+ const existing = preToolUse.find((entry) => isGuardEntry(entry, marker2));
5498
5755
  if (existing) {
5499
- const fresh = buildHookEntry(hookCommand);
5756
+ const fresh = buildHookEntry(hookCommand, matcher, extraHookFields);
5500
5757
  const changed = JSON.stringify(existing) !== JSON.stringify(fresh);
5501
5758
  if (changed) {
5502
5759
  preToolUse[preToolUse.indexOf(existing)] = fresh;
@@ -5504,11 +5761,11 @@ function installClaudeHooks(settingsPath, hookCommand) {
5504
5761
  }
5505
5762
  return { installed: false, alreadyPresent: true, updated: changed };
5506
5763
  }
5507
- preToolUse.push(buildHookEntry(hookCommand));
5764
+ preToolUse.push(buildHookEntry(hookCommand, matcher, extraHookFields));
5508
5765
  writeSettings(settingsPath, settings);
5509
5766
  return { installed: true, alreadyPresent: false, updated: false };
5510
5767
  }
5511
- function uninstallClaudeHooks(settingsPath) {
5768
+ function uninstallClaudeHooks(settingsPath, marker2 = ASTRASYNC_HOOK_MARKER) {
5512
5769
  if (!(0, import_fs5.existsSync)(settingsPath)) return false;
5513
5770
  let settings;
5514
5771
  try {
@@ -5518,7 +5775,7 @@ function uninstallClaudeHooks(settingsPath) {
5518
5775
  }
5519
5776
  const preToolUse = settings.hooks?.PreToolUse;
5520
5777
  if (!preToolUse) return false;
5521
- const remaining = preToolUse.filter((entry) => !isGuardEntry(entry));
5778
+ const remaining = preToolUse.filter((entry) => !isGuardEntry(entry, marker2));
5522
5779
  if (remaining.length === preToolUse.length) return false;
5523
5780
  if (remaining.length > 0) {
5524
5781
  settings.hooks.PreToolUse = remaining;
@@ -5532,7 +5789,9 @@ function uninstallClaudeHooks(settingsPath) {
5532
5789
  var GUARD_SELF_PROTECTION_PATTERNS = [
5533
5790
  "**/.claude/settings.json",
5534
5791
  "**/.claude/settings.local.json",
5535
- "**/.astrasync/**"
5792
+ "**/.astrasync/**",
5793
+ "**/.codex/hooks.json",
5794
+ "**/.codex/config.toml"
5536
5795
  ];
5537
5796
  function ensureGuardSelfProtection(policyPath) {
5538
5797
  const parsed = parseFile(policyPath);
@@ -5579,6 +5838,25 @@ function buildDefaultClaudeCodePolicy(name = "claude-code-local-guard") {
5579
5838
  };
5580
5839
  }
5581
5840
 
5841
+ // src/codex/settings-installer.ts
5842
+ var ASTRASYNC_CODEX_HOOK_MARKER = "astrasync-codex-hook";
5843
+ var CODEX_HOOK_MATCHER = "Bash|apply_patch|Edit|Write|web_search|mcp__.*";
5844
+ var CODEX_STATUS_MESSAGE = "AstraSync Local Guard";
5845
+ function installCodexHooks(hooksPath, hookCommand) {
5846
+ return installClaudeHooks(
5847
+ hooksPath,
5848
+ hookCommand,
5849
+ ASTRASYNC_CODEX_HOOK_MARKER,
5850
+ CODEX_HOOK_MATCHER,
5851
+ {
5852
+ statusMessage: CODEX_STATUS_MESSAGE
5853
+ }
5854
+ );
5855
+ }
5856
+ function uninstallCodexHooks(hooksPath) {
5857
+ return uninstallClaudeHooks(hooksPath, ASTRASYNC_CODEX_HOOK_MARKER);
5858
+ }
5859
+
5582
5860
  // src/cli/guard.ts
5583
5861
  var DEFAULT_POLICY_PATH = "./local-policy.yaml";
5584
5862
  var DEFAULT_CONFIG_DIR = ".astrasync";
@@ -5744,6 +6022,7 @@ async function cmdStatus() {
5744
6022
  console.log(' Policy: not found \u2014 run "astrasync guard setup"');
5745
6023
  }
5746
6024
  printClaudeCodeStatus();
6025
+ printCodexStatus();
5747
6026
  console.log("");
5748
6027
  }
5749
6028
  async function cmdCheck() {
@@ -5830,7 +6109,7 @@ async function cmdUninstallHooks() {
5830
6109
  function resolveClaudeHookCommand() {
5831
6110
  const sibling = path.join(__dirname, "astrasync-claude-hook.js");
5832
6111
  if (fs.existsSync(sibling)) {
5833
- return `node "${sibling}"`;
6112
+ return `"${process.execPath}" "${sibling}"`;
5834
6113
  }
5835
6114
  return "astrasync-claude-hook";
5836
6115
  }
@@ -5922,6 +6201,108 @@ async function cmdUninstallClaudeCode() {
5922
6201
  `[AstraSync] Policy and traces left in place at ${astrasyncDir} \u2014 delete manually if unwanted.`
5923
6202
  );
5924
6203
  }
6204
+ function resolveCodexHookCommand() {
6205
+ const sibling = path.join(__dirname, "astrasync-codex-hook.js");
6206
+ if (fs.existsSync(sibling)) {
6207
+ return `"${process.execPath}" "${sibling}"`;
6208
+ }
6209
+ return "astrasync-codex-hook";
6210
+ }
6211
+ function codexPaths(project) {
6212
+ const root = project ? process.cwd() : os.homedir();
6213
+ return {
6214
+ hooksPath: path.join(root, ".codex", "hooks.json"),
6215
+ astrasyncDir: path.join(root, ".astrasync")
6216
+ };
6217
+ }
6218
+ async function cmdInstallCodex() {
6219
+ const flags = parseClaudeCodeFlags();
6220
+ const { hooksPath, astrasyncDir } = codexPaths(flags.project);
6221
+ const policyPath = flags.policyOverride || path.join(astrasyncDir, "policy.yaml");
6222
+ const guardJsonPath = path.join(astrasyncDir, "guard.json");
6223
+ console.log("\nAstraSync Local Guard for Codex CLI \u2014 install\n");
6224
+ if (fs.existsSync(policyPath)) {
6225
+ console.log(` Policy: using existing ${policyPath} (shared across adapters)`);
6226
+ if (ensureGuardSelfProtection(policyPath)) {
6227
+ console.log(" added guard self-protection patterns to file.write");
6228
+ }
6229
+ } else if (flags.wizard) {
6230
+ const prompt = createPrompt();
6231
+ try {
6232
+ fs.mkdirSync(path.dirname(policyPath), { recursive: true });
6233
+ const wizard = new SetupWizard(prompt);
6234
+ await wizard.runAndSave(policyPath);
6235
+ } finally {
6236
+ prompt.close();
6237
+ }
6238
+ ensureGuardSelfProtection(policyPath);
6239
+ console.log(` Policy: created via wizard at ${policyPath}`);
6240
+ } else {
6241
+ fs.mkdirSync(path.dirname(policyPath), { recursive: true });
6242
+ fs.writeFileSync(policyPath, toYaml(buildDefaultClaudeCodePolicy()), "utf-8");
6243
+ console.log(` Policy: default coding-agent policy written to ${policyPath}`);
6244
+ console.log(" (re-run with --wizard for the interactive setup instead)");
6245
+ }
6246
+ if (!fs.existsSync(guardJsonPath)) {
6247
+ const guardSettings = {
6248
+ posture: flags.active ? "active" : "passive",
6249
+ governMcp: false
6250
+ };
6251
+ fs.writeFileSync(guardJsonPath, `${JSON.stringify(guardSettings, null, 2)}
6252
+ `, "utf-8");
6253
+ console.log(
6254
+ ` Guard: ${guardJsonPath} (posture: ${guardSettings.posture}, governMcp: false)`
6255
+ );
6256
+ } else {
6257
+ console.log(` Guard: using existing ${guardJsonPath}`);
6258
+ }
6259
+ const hookCommand = resolveCodexHookCommand();
6260
+ const result = installCodexHooks(hooksPath, hookCommand);
6261
+ if (result.installed) {
6262
+ console.log(` Hook: installed in ${hooksPath}`);
6263
+ } else if (result.updated) {
6264
+ console.log(` Hook: updated in ${hooksPath}`);
6265
+ } else {
6266
+ console.log(` Hook: already present in ${hooksPath}`);
6267
+ }
6268
+ console.log("\n Next steps:");
6269
+ console.log(" 1. Start a NEW Codex session (hooks load at startup; /hooks to inspect).");
6270
+ console.log(` 2. Watch decisions: tail -f ${path.join(astrasyncDir, "traces")}/*.jsonl`);
6271
+ console.log(` 3. When the traces look right, set "posture": "active" in ${guardJsonPath}.`);
6272
+ console.log(" Denies block; approvals fall through to Codex's own prompt \u2014");
6273
+ console.log(" except in dontAsk/bypassPermissions modes, where they deny.");
6274
+ console.log(" 4. Note: Codex fails OPEN if the hook itself times out; the guard");
6275
+ console.log(" fails closed on every failure it can see.\n");
6276
+ }
6277
+ async function cmdUninstallCodex() {
6278
+ const flags = parseClaudeCodeFlags();
6279
+ const { hooksPath, astrasyncDir } = codexPaths(flags.project);
6280
+ const removed = uninstallCodexHooks(hooksPath);
6281
+ if (removed) {
6282
+ console.log(`[AstraSync] Removed Codex hook from ${hooksPath}`);
6283
+ } else {
6284
+ console.log(`[AstraSync] No AstraSync hook found in ${hooksPath}`);
6285
+ }
6286
+ console.log(
6287
+ `[AstraSync] Policy and traces left in place at ${astrasyncDir} \u2014 delete manually if unwanted.`
6288
+ );
6289
+ }
6290
+ function printCodexStatus() {
6291
+ for (const scope of ["user", "project"]) {
6292
+ const { hooksPath, astrasyncDir } = codexPaths(scope === "project");
6293
+ let hookInstalled = false;
6294
+ try {
6295
+ const content = fs.readFileSync(hooksPath, "utf-8");
6296
+ hookInstalled = content.includes(ASTRASYNC_CODEX_HOOK_MARKER);
6297
+ } catch {
6298
+ }
6299
+ if (hookInstalled) {
6300
+ const policyPath = path.join(astrasyncDir, "policy.yaml");
6301
+ const policy = fs.existsSync(policyPath) ? `policy: ${policyPath}` : "policy: missing";
6302
+ console.log(` Codex CLI (${scope}): hook installed, ${policy}`);
6303
+ }
6304
+ }
6305
+ }
5925
6306
  function printClaudeCodeStatus() {
5926
6307
  for (const scope of ["user", "project"]) {
5927
6308
  const { settingsPath, astrasyncDir } = claudeCodePaths(scope === "project");
@@ -5979,6 +6360,12 @@ async function main() {
5979
6360
  case "uninstall-claude-code":
5980
6361
  await cmdUninstallClaudeCode();
5981
6362
  break;
6363
+ case "install-codex":
6364
+ await cmdInstallCodex();
6365
+ break;
6366
+ case "uninstall-codex":
6367
+ await cmdUninstallCodex();
6368
+ break;
5982
6369
  default:
5983
6370
  console.log("AstraSync Local Guard\n");
5984
6371
  console.log("Usage: astrasync guard <command>\n");
@@ -5996,6 +6383,9 @@ async function main() {
5996
6383
  console.log(
5997
6384
  " uninstall-claude-code Remove the Claude Code hook (--project for project scope)"
5998
6385
  );
6386
+ console.log(" install-codex Install the Local Guard hook for OpenAI Codex CLI");
6387
+ console.log(" (same flags; policy shared with Claude Code)");
6388
+ console.log(" uninstall-codex Remove the Codex hook (--project for project scope)");
5999
6389
  console.log("");
6000
6390
  process.exit(command ? 1 : 0);
6001
6391
  }