@codai/axiom-mcp 2.1.0 → 2.2.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.
package/dist/gate-lazy.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { realpath } from "node:fs";
2
2
  import * as fs$1 from "node:fs/promises";
3
- import { readFile } from "node:fs/promises";
3
+ import { lstat, readFile } from "node:fs/promises";
4
4
  import { cpus, homedir } from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { parseArgs, promisify } from "node:util";
@@ -5680,18 +5680,26 @@ const ERROR_CODES = [
5680
5680
  "ERR_PROVIDER_FAILED",
5681
5681
  "ERR_GUARD_TIMEOUT",
5682
5682
  "ERR_GUARD_OUTPUT",
5683
+ "ERR_FACT_DISABLED",
5683
5684
  "ERR_SIGNATURE_MISSING",
5684
5685
  "ERR_SIGNATURE_INVALID",
5686
+ "ERR_TRUST_STATE_CORRUPT",
5685
5687
  "ERR_ROLLBACK",
5686
5688
  "ERR_EMITTER_UNKNOWN",
5687
5689
  "ERR_TEMPLATE_UNKNOWN",
5688
5690
  "ERR_TEMPLATE_PARAMS",
5691
+ "ERR_PATCH_FORMAT",
5692
+ "ERR_PATCH_PREIMAGE",
5693
+ "ERR_PATCH_NO_MATCH",
5689
5694
  "ERR_GIT_NOT_FOUND",
5690
5695
  "ERR_GIT_NOT_REPO",
5691
5696
  "ERR_GIT_DIRTY",
5692
5697
  "ERR_GIT_BRANCH_EXISTS",
5693
5698
  "ERR_GIT_BRANCH_INVALID",
5694
5699
  "ERR_GIT_FAILED",
5700
+ "ERR_TASK_NOT_FOUND",
5701
+ "ERR_TASK_CANCELLED",
5702
+ "ERR_PLAN_SESSION_STATE",
5695
5703
  "ERR_REF_OFFLINE",
5696
5704
  "ERR_NET_DISABLED",
5697
5705
  "ERR_NET_DENIED",
@@ -5799,7 +5807,12 @@ object({
5799
5807
  findings: array(FindingSchema),
5800
5808
  factsDigest: DigestRefSchema,
5801
5809
  durationMs: int().nonnegative(),
5802
- providers: array(ProviderStatusSchema)
5810
+ providers: array(ProviderStatusSchema),
5811
+ preImage: _enum([
5812
+ "verified",
5813
+ "drifted",
5814
+ "unverified"
5815
+ ]).optional()
5803
5816
  }).strict();
5804
5817
  const ApiVersionSchema = literal("axiom.dev/v2");
5805
5818
  const INLINE_CONTENT_MAX = 262144;
@@ -5838,7 +5851,17 @@ const PlanArtifactSourceSchema = discriminatedUnion("type", [
5838
5851
  emitter: string().min(1),
5839
5852
  template: string().min(1),
5840
5853
  params: record(string(), json()).default({})
5841
- }).strict().describe("v2.1")
5854
+ }).strict().describe("template source rendered by a registered emitter at compile time"),
5855
+ object({
5856
+ type: literal("patch"),
5857
+ format: _enum([
5858
+ "unified",
5859
+ "v4a",
5860
+ "search-replace"
5861
+ ]),
5862
+ preImage: union([DigestRefSchema, literal("absent")]),
5863
+ body: string().max(INLINE_CONTENT_MAX)
5864
+ }).strict().describe("patch applied at compile time to the pre-image named by its digest")
5842
5865
  ]);
5843
5866
  const PlanArtifactSchema = object({
5844
5867
  path: RelPathSchema,
@@ -5913,6 +5936,7 @@ object({
5913
5936
  diff: string().optional(),
5914
5937
  journal: string().optional(),
5915
5938
  git: GitResultSchema.optional(),
5939
+ drifted: array(RelPathSchema).optional(),
5916
5940
  error: ApplyErrorSchema.optional()
5917
5941
  }).strict().superRefine((r, ctx) => {
5918
5942
  if (r.status === "failed" && r.error === void 0) ctx.addIssue({
@@ -5973,7 +5997,8 @@ const ManifestArtifactSchema = object({
5973
5997
  "inline",
5974
5998
  "template",
5975
5999
  "cas",
5976
- "ref"
6000
+ "ref",
6001
+ "patch"
5977
6002
  ]).optional()
5978
6003
  }).strict().superRefine((a, ctx) => {
5979
6004
  if (a.op !== "delete" && a.digest === void 0) ctx.addIssue({
@@ -5987,6 +6012,10 @@ const ToolchainSchema = object({
5987
6012
  axiom: string().min(1),
5988
6013
  emitters: record(string(), string())
5989
6014
  }).strict();
6015
+ const PreImageEntrySchema = object({
6016
+ path: RelPathSchema,
6017
+ sha256: union([Sha256HexSchema, literal("absent")])
6018
+ }).strict();
5990
6019
  const ManifestBodySchema = object({
5991
6020
  apiVersion: ApiVersionSchema,
5992
6021
  kind: literal("Manifest"),
@@ -5996,8 +6025,15 @@ const ManifestBodySchema = object({
5996
6025
  artifacts: array(ManifestArtifactSchema).min(1).max(2e3),
5997
6026
  checks: array(CheckRefSchema),
5998
6027
  toolchain: ToolchainSchema,
5999
- counter: int().nonnegative().optional()
6028
+ counter: int().nonnegative().optional(),
6029
+ preImage: array(PreImageEntrySchema).optional()
6000
6030
  }).strict().superRefine((m, ctx) => {
6031
+ if (m.preImage !== void 0 && !isSortedUnique(m.preImage.map((p) => p.path))) ctx.addIssue({
6032
+ code: "custom",
6033
+ path: ["preImage"],
6034
+ message: "preImage must be sorted by path (UTF-8 byte order) and unique",
6035
+ params: { code: "ERR_NOT_CANONICAL" }
6036
+ });
6001
6037
  if (!isSortedUnique(m.artifacts.map((a) => a.path))) ctx.addIssue({
6002
6038
  code: "custom",
6003
6039
  path: ["artifacts"],
@@ -6037,8 +6073,11 @@ const DsseEnvelopeSchema = object({
6037
6073
  sig: base64()
6038
6074
  }).strict())
6039
6075
  }).strict();
6076
+ const AXIOM_MANIFEST_PAYLOAD_TYPE = "application/vnd.axiom.manifest+json";
6077
+ const AXIOM_MANIFEST_BOUND_PAYLOAD_TYPE = "application/vnd.axiom.manifest-bound+json";
6078
+ const RootIdSchema = string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:/@-]*$/, "rootId: letters, digits, . _ : / @ - only");
6040
6079
  const ManifestSignatureSchema = object({
6041
- payloadType: literal("application/vnd.axiom.manifest+json"),
6080
+ payloadType: union([literal(AXIOM_MANIFEST_PAYLOAD_TYPE), literal(AXIOM_MANIFEST_BOUND_PAYLOAD_TYPE)]),
6042
6081
  payload: base64(),
6043
6082
  signatures: array(object({
6044
6083
  keyid: string().optional(),
@@ -6055,12 +6094,14 @@ const TrustedKeySchema = object({
6055
6094
  object({
6056
6095
  version: literal(1),
6057
6096
  keys: array(TrustedKeySchema),
6058
- minCounter: int().nonnegative().optional()
6097
+ minCounter: int().nonnegative().optional(),
6098
+ rootId: RootIdSchema.optional()
6059
6099
  }).strict();
6060
6100
  object({
6061
6101
  version: literal(1),
6062
6102
  lastCounter: int().nonnegative(),
6063
- manifestDigest: DigestRefSchema.optional()
6103
+ manifestDigest: DigestRefSchema.optional(),
6104
+ mac: string().regex(/^[0-9a-f]{64}$/).optional()
6064
6105
  }).strict();
6065
6106
  object({
6066
6107
  manifest: ManifestBodySchema,
@@ -8290,9 +8331,14 @@ var import_picomatch = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((
8290
8331
  function definePredicate(p) {
8291
8332
  return p;
8292
8333
  }
8334
+ function escapeRouteGroups(glob) {
8335
+ return glob.replace(/(^|[^\\@!?*+])\(([^()|*?[\]{}\\]+)\)/g, (_m, pre, inner) => {
8336
+ return `${pre}\\(${inner}\\)`;
8337
+ });
8338
+ }
8293
8339
  function globMatcher(globs, whenEmpty) {
8294
8340
  if (globs === void 0 || globs.length === 0) return () => whenEmpty;
8295
- const m = (0, import_picomatch.default)([...globs], { dot: true });
8341
+ const m = (0, import_picomatch.default)(globs.map(escapeRouteGroups), { dot: true });
8296
8342
  return (p) => m(p);
8297
8343
  }
8298
8344
  function finding(f) {
@@ -8315,60 +8361,156 @@ object({
8315
8361
  "info"
8316
8362
  ]).optional()
8317
8363
  }).strict();
8364
+ object({
8365
+ policies: string().min(1).max(65536),
8366
+ mode: _enum(["forbid", "permit"]).default("forbid"),
8367
+ message: string().max(2e3).optional(),
8368
+ severity: _enum([
8369
+ "error",
8370
+ "warn",
8371
+ "info"
8372
+ ]).optional()
8373
+ }).strict();
8374
+ function luhnValid(digits) {
8375
+ let sum = 0;
8376
+ let double = false;
8377
+ for (let i = digits.length - 1; i >= 0; i--) {
8378
+ let d = digits.charCodeAt(i) - 48;
8379
+ if (d < 0 || d > 9) return false;
8380
+ if (double) {
8381
+ d *= 2;
8382
+ if (d > 9) d -= 9;
8383
+ }
8384
+ sum += d;
8385
+ double = !double;
8386
+ }
8387
+ return digits.length > 0 && sum % 10 === 0;
8388
+ }
8389
+ const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
8390
+ const CARD_RE = /(?<![\w-])(?:\d[ -]?){12,15}\d(?![\w-])/g;
8391
+ function findCardNumber(text) {
8392
+ const scrubbed = text.replace(UUID_RE, (m) => " ".repeat(m.length));
8393
+ CARD_RE.lastIndex = 0;
8394
+ for (let m = CARD_RE.exec(scrubbed); m !== null; m = CARD_RE.exec(scrubbed)) {
8395
+ const digits = m[0].replace(/[ -]/g, "");
8396
+ if (digits.length < 13 || digits.length > 16) continue;
8397
+ if (/^(\d)\1+$/.test(digits)) continue;
8398
+ if (luhnValid(digits)) return true;
8399
+ }
8400
+ return false;
8401
+ }
8402
+ function cnpValid(digits) {
8403
+ if (!/^[1-9]\d{12}$/.test(digits)) return false;
8404
+ const mm = Number(digits.slice(3, 5));
8405
+ const dd = Number(digits.slice(5, 7));
8406
+ const jj = Number(digits.slice(7, 9));
8407
+ if (mm < 1 || mm > 12 || dd < 1 || dd > 31) return false;
8408
+ if (!(jj >= 1 && jj <= 52 || jj === 70)) return false;
8409
+ const w = "279146358279";
8410
+ let sum = 0;
8411
+ for (let i = 0; i < 12; i++) sum += (digits.charCodeAt(i) - 48) * (w.charCodeAt(i) - 48);
8412
+ return (sum % 11 === 10 ? 1 : sum % 11) === digits.charCodeAt(12) - 48;
8413
+ }
8414
+ const CNP_RE = /\b[1-9]\d{12}\b/g;
8415
+ function findCnp(text) {
8416
+ CNP_RE.lastIndex = 0;
8417
+ for (let m = CNP_RE.exec(text); m !== null; m = CNP_RE.exec(text)) if (cnpValid(m[0])) return true;
8418
+ return false;
8419
+ }
8420
+ const EMAIL_RE = /\b([A-Za-z0-9._%+-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/g;
8421
+ const EMAIL_SKIP_DOMAIN = /(^|\.)(example\.(com|net|org)|example|test|invalid|localhost|localdomain)$|(^|\.)github\.com$/i;
8422
+ const EMAIL_SKIP_LOCAL = /^(git|noreply|no-reply|donotreply|do-not-reply)$/i;
8423
+ const EMAIL_SKIP_TLD_AS_EXT = /\.(png|jpe?g|gif|svg|webp|ico|css|js|ts|json|md|txt|html?|xml|yml|yaml|py)$/i;
8424
+ function findPersonalEmail(text) {
8425
+ EMAIL_RE.lastIndex = 0;
8426
+ for (let m = EMAIL_RE.exec(text); m !== null; m = EMAIL_RE.exec(text)) {
8427
+ const local = m[1] ?? "";
8428
+ const domain = m[2] ?? "";
8429
+ if (EMAIL_SKIP_DOMAIN.test(domain)) continue;
8430
+ if (EMAIL_SKIP_LOCAL.test(local)) continue;
8431
+ if (EMAIL_SKIP_TLD_AS_EXT.test(domain)) continue;
8432
+ return true;
8433
+ }
8434
+ return false;
8435
+ }
8436
+ const CRED_RE = /(?:^|[^A-Za-z0-9])[A-Za-z0-9_-]*?(password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|auth[_-]?token|client[_-]?secret)\b\s*[:=]\s*(?:new\s+\w+\s*\(\s*)?(["'`])([^"'`\r\n]{8,})\2/gi;
8437
+ const CRED_PLACEHOLDER = /^(\$\{|\{\{|<|%[(sd]|\$[A-Z_]|xxx|\*{3,}|\.{3,}|change[_-]?me|replace[_-]?me|your[_-]|my[_-]?(actual[_-]?)?(password|secret|token|key)|placeholder|example|dummy|sample|test(value|ing)?$|redacted|hunter2$|password$|secret$|123456)/i;
8438
+ function findCredentialLiteral(text) {
8439
+ CRED_RE.lastIndex = 0;
8440
+ for (let m = CRED_RE.exec(text); m !== null; m = CRED_RE.exec(text)) {
8441
+ const value = m[3] ?? "";
8442
+ if (CRED_PLACEHOLDER.test(value)) continue;
8443
+ if (/^[a-z_][a-z0-9_.]*$/i.test(value) && !/\d/.test(value)) continue;
8444
+ return true;
8445
+ }
8446
+ return false;
8447
+ }
8318
8448
  const SECRET_PATTERNS = [
8319
8449
  {
8320
8450
  name: "cnp",
8321
- re: /\b[1-9]\d{12}\b/
8451
+ kind: "pii",
8452
+ test: findCnp
8322
8453
  },
8323
8454
  {
8324
8455
  name: "email",
8325
- re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/
8456
+ kind: "pii",
8457
+ test: findPersonalEmail
8326
8458
  },
8327
8459
  {
8328
8460
  name: "phoneRo",
8461
+ kind: "pii",
8329
8462
  re: /\b(\+4|0)7\d{8}\b/
8330
8463
  },
8331
8464
  {
8332
8465
  name: "card",
8333
- re: /\b(?:\d[ -]?){13,16}\b/
8466
+ kind: "pii",
8467
+ test: findCardNumber
8334
8468
  },
8335
8469
  {
8336
8470
  name: "credentialAssignment",
8337
- re: /\b(password|secret|token|api[_-]?key)\b\s*[:=]\s*["']?[^\s"']{4,}/i
8471
+ kind: "secret",
8472
+ test: findCredentialLiteral
8338
8473
  },
8339
8474
  {
8340
8475
  name: "awsKey",
8476
+ kind: "secret",
8341
8477
  re: /AKIA[0-9A-Z]{16}/
8342
8478
  },
8343
8479
  {
8344
8480
  name: "githubToken",
8481
+ kind: "secret",
8345
8482
  re: /gh[pousr]_[A-Za-z0-9]{36,}/
8346
8483
  },
8347
8484
  {
8348
8485
  name: "privateKey",
8486
+ kind: "secret",
8349
8487
  re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/
8350
8488
  },
8351
8489
  {
8352
8490
  name: "jwt",
8491
+ kind: "secret",
8353
8492
  re: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/
8354
8493
  },
8355
8494
  {
8356
8495
  name: "slackToken",
8496
+ kind: "secret",
8357
8497
  re: /xox[abpr]-/
8358
8498
  }
8359
8499
  ];
8360
8500
  SECRET_PATTERNS.map((p) => p.name);
8501
+ SECRET_PATTERNS.filter((p) => p.kind === "pii").map((p) => p.name);
8361
8502
  const SCAN_LIMIT = 4194304;
8362
8503
  const contentNoSecrets = definePredicate({
8363
8504
  id: "content.noSecrets",
8364
8505
  params: object({
8365
8506
  disable: array(string()).default([]),
8366
- allowPaths: array(string().min(1)).default([])
8507
+ allowPaths: array(string().min(1)).default([]),
8508
+ pii: boolean().default(false)
8367
8509
  }).strict(),
8368
8510
  requires: ["manifest", "content"],
8369
- async run(ctx, { disable, allowPaths }) {
8511
+ async run(ctx, { disable, allowPaths, pii }) {
8370
8512
  const skip = globMatcher(allowPaths, false);
8371
- const active = SECRET_PATTERNS.filter((p) => !disable.includes(p.name));
8513
+ const active = SECRET_PATTERNS.filter((p) => !disable.includes(p.name) && (pii || p.kind === "secret"));
8372
8514
  const out = [];
8373
8515
  const decoder = new TextDecoder("utf-8", { fatal: false });
8374
8516
  for (const a of ctx.manifest.artifacts) {
@@ -8377,7 +8519,7 @@ const contentNoSecrets = definePredicate({
8377
8519
  if (bytes === void 0) continue;
8378
8520
  const text = decoder.decode(bytes.subarray(0, SCAN_LIMIT));
8379
8521
  for (const p of active) {
8380
- if (!p.re.test(text)) continue;
8522
+ if (!(p.test !== void 0 ? p.test(text) : p.re?.test(text) ?? false)) continue;
8381
8523
  out.push(finding({
8382
8524
  id: `content.noSecrets.${p.name}`,
8383
8525
  predicate: "content.noSecrets",
@@ -8433,7 +8575,7 @@ object({
8433
8575
  command: string().min(1),
8434
8576
  args: array(string()).default([]),
8435
8577
  cwd: _enum(["root", "staging"]).default("root"),
8436
- timeoutMs: int().min(1).max(6e4).default(3e4),
8578
+ timeoutMs: int().min(1).max(9e5).default(3e4),
8437
8579
  env: record(string(), string()).optional(),
8438
8580
  stdin: _enum([
8439
8581
  "bundle",
@@ -8497,7 +8639,8 @@ object({ rules: array(object({
8497
8639
  when: string().min(1),
8498
8640
  expect: array(object({
8499
8641
  name: string().min(1),
8500
- match: string().min(1)
8642
+ match: string().min(1),
8643
+ mustChange: boolean().default(false)
8501
8644
  }).strict()).min(1)
8502
8645
  }).strict()).min(1) }).strict();
8503
8646
  object({
@@ -8516,6 +8659,7 @@ const GateProfileSchema = object({
8516
8659
  deny: array(string().min(1)).default([]),
8517
8660
  allow: array(string().min(1)).optional(),
8518
8661
  noSecrets: boolean().default(true),
8662
+ pii: boolean().default(false),
8519
8663
  maxBytes: int().positive().optional()
8520
8664
  }).strict();
8521
8665
  const DEFAULT_GATE_PROFILE = {
@@ -8528,7 +8672,8 @@ const DEFAULT_GATE_PROFILE = {
8528
8672
  ".env.*",
8529
8673
  "**/node_modules/**"
8530
8674
  ],
8531
- noSecrets: true
8675
+ noSecrets: true,
8676
+ pii: false
8532
8677
  };
8533
8678
  async function loadGateProfile(opts) {
8534
8679
  const candidates = [];
@@ -8671,6 +8816,262 @@ function isWriteTool(toolName) {
8671
8816
  if (n.includes("apply_patch") || n.includes("multi_replace")) return true;
8672
8817
  return !NON_WRITE_HINTS.some((h) => n.includes(h));
8673
8818
  }
8819
+ const SHELL_TOOL_HINTS = [
8820
+ "bash",
8821
+ "shell",
8822
+ "run_in_terminal",
8823
+ "terminal",
8824
+ "execute_command",
8825
+ "exec_command",
8826
+ "run_command",
8827
+ "powershell",
8828
+ "cmd"
8829
+ ];
8830
+ const SHELL_COMMAND_KEYS = [
8831
+ "command",
8832
+ "cmd",
8833
+ "script",
8834
+ "commandLine",
8835
+ "command_line"
8836
+ ];
8837
+ function isShellTool(toolName) {
8838
+ const n = toolName.toLowerCase();
8839
+ return SHELL_TOOL_HINTS.some((h) => n.includes(h));
8840
+ }
8841
+ function classifyTool(toolName) {
8842
+ if (isWriteTool(toolName)) return "write";
8843
+ if (isShellTool(toolName)) return "shell";
8844
+ return "other";
8845
+ }
8846
+ const REDIRECT_RE = /(?:^|[\s;&|])(?:\d?>{1,2}|&>)\s*("([^"]+)"|'([^']+)'|([^\s;&|<>]+))/g;
8847
+ const SHELL_SPLIT_RE = /(?:^|[;&|]+|\|\||&&)\s*/;
8848
+ const WRITE_CMDS = {
8849
+ tee: {
8850
+ skipFlags: true,
8851
+ operands: "all"
8852
+ },
8853
+ rm: {
8854
+ skipFlags: true,
8855
+ operands: "all"
8856
+ },
8857
+ rmdir: {
8858
+ skipFlags: true,
8859
+ operands: "all"
8860
+ },
8861
+ mv: {
8862
+ skipFlags: true,
8863
+ operands: "last"
8864
+ },
8865
+ cp: {
8866
+ skipFlags: true,
8867
+ operands: "last"
8868
+ },
8869
+ touch: {
8870
+ skipFlags: true,
8871
+ operands: "all"
8872
+ },
8873
+ truncate: {
8874
+ skipFlags: true,
8875
+ operands: "all"
8876
+ },
8877
+ mkdir: {
8878
+ skipFlags: true,
8879
+ operands: "all"
8880
+ },
8881
+ ln: {
8882
+ skipFlags: true,
8883
+ operands: "last"
8884
+ },
8885
+ chmod: {
8886
+ skipFlags: true,
8887
+ operands: "all"
8888
+ },
8889
+ unlink: {
8890
+ skipFlags: true,
8891
+ operands: "all"
8892
+ },
8893
+ install: {
8894
+ skipFlags: true,
8895
+ operands: "last"
8896
+ },
8897
+ dd: {
8898
+ skipFlags: false,
8899
+ operands: "all"
8900
+ },
8901
+ sed: {
8902
+ skipFlags: true,
8903
+ operands: "afterI"
8904
+ },
8905
+ "remove-item": {
8906
+ skipFlags: true,
8907
+ operands: "all"
8908
+ },
8909
+ "set-content": {
8910
+ skipFlags: true,
8911
+ operands: "all"
8912
+ },
8913
+ "add-content": {
8914
+ skipFlags: true,
8915
+ operands: "all"
8916
+ },
8917
+ "out-file": {
8918
+ skipFlags: true,
8919
+ operands: "all"
8920
+ },
8921
+ "move-item": {
8922
+ skipFlags: true,
8923
+ operands: "all"
8924
+ },
8925
+ "copy-item": {
8926
+ skipFlags: true,
8927
+ operands: "all"
8928
+ },
8929
+ "new-item": {
8930
+ skipFlags: true,
8931
+ operands: "all"
8932
+ },
8933
+ del: {
8934
+ skipFlags: true,
8935
+ operands: "all"
8936
+ },
8937
+ erase: {
8938
+ skipFlags: true,
8939
+ operands: "all"
8940
+ },
8941
+ move: {
8942
+ skipFlags: true,
8943
+ operands: "all"
8944
+ },
8945
+ copy: {
8946
+ skipFlags: true,
8947
+ operands: "all"
8948
+ }
8949
+ };
8950
+ const GIT_WRITE_SUBCMDS = /* @__PURE__ */ new Set([
8951
+ "checkout",
8952
+ "reset",
8953
+ "clean",
8954
+ "restore",
8955
+ "rm",
8956
+ "mv",
8957
+ "stash"
8958
+ ]);
8959
+ function shellWords(segment) {
8960
+ const out = [];
8961
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
8962
+ for (let m = re.exec(segment); m !== null; m = re.exec(segment)) out.push(m[1] ?? m[2] ?? m[3] ?? "");
8963
+ return out;
8964
+ }
8965
+ function looksLikePath(word) {
8966
+ if (word === "" || word === "-" || word === "--") return false;
8967
+ if (word.startsWith("-") && !word.startsWith("./") && !word.startsWith("../")) return false;
8968
+ if (/^\$\(|^`|^\$[A-Za-z_{]/.test(word)) return false;
8969
+ return true;
8970
+ }
8971
+ function extractShellWriteTargets(command) {
8972
+ const out = [];
8973
+ const seen = /* @__PURE__ */ new Set();
8974
+ const push = (rawPath, op) => {
8975
+ if (!looksLikePath(rawPath) || seen.has(rawPath)) return;
8976
+ seen.add(rawPath);
8977
+ out.push({
8978
+ rawPath,
8979
+ op,
8980
+ content: void 0
8981
+ });
8982
+ };
8983
+ for (let m = REDIRECT_RE.exec(command); m !== null; m = REDIRECT_RE.exec(command)) {
8984
+ const target = m[2] ?? m[3] ?? m[4] ?? "";
8985
+ if (target !== "" && !/^&\d$/.test(target) && !/^\/dev\//.test(target)) push(target, "write");
8986
+ }
8987
+ REDIRECT_RE.lastIndex = 0;
8988
+ for (const seg of command.split(SHELL_SPLIT_RE)) {
8989
+ const words = shellWords(seg);
8990
+ let i = 0;
8991
+ while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i] ?? "")) i++;
8992
+ while (i < words.length && [
8993
+ "sudo",
8994
+ "exec",
8995
+ "command",
8996
+ "nohup",
8997
+ "time"
8998
+ ].includes(words[i] ?? "")) i++;
8999
+ const cmd = (words[i] ?? "").toLowerCase().replace(/^.*[\\/]/, "");
9000
+ const rest = words.slice(i + 1);
9001
+ if (cmd === "git") {
9002
+ const sub = rest.find((w) => !w.startsWith("-"));
9003
+ if (sub !== void 0 && GIT_WRITE_SUBCMDS.has(sub)) {
9004
+ const afterSub = rest.slice(rest.indexOf(sub) + 1);
9005
+ const dd = afterSub.indexOf("--");
9006
+ const operands = dd === -1 ? afterSub.filter((w) => !w.startsWith("-")) : afterSub.slice(dd + 1);
9007
+ if (sub === "clean" || sub === "reset" && afterSub.includes("--hard")) push(".", "delete");
9008
+ for (const p of operands) push(p, sub === "rm" || sub === "clean" ? "delete" : "write");
9009
+ }
9010
+ continue;
9011
+ }
9012
+ const rule = WRITE_CMDS[cmd];
9013
+ if (rule === void 0) continue;
9014
+ if (cmd === "sed") {
9015
+ if (!rest.some((w) => /^-[a-zA-Z]*i/.test(w) || w === "--in-place" || w.startsWith("--in-place="))) continue;
9016
+ const hasScriptFlag = rest.some((w) => /^-[a-zA-Z]*[ef]/.test(w) || /^--(expression|file)/.test(w));
9017
+ const nonFlags = [];
9018
+ for (let k = 0; k < rest.length; k++) {
9019
+ const w = rest[k] ?? "";
9020
+ if (w.startsWith("-")) {
9021
+ if (/^-[a-zA-Z]*[ef]$/.test(w) || w === "--expression" || w === "--file") k++;
9022
+ continue;
9023
+ }
9024
+ nonFlags.push(w);
9025
+ }
9026
+ const files = hasScriptFlag ? nonFlags : nonFlags.slice(1);
9027
+ for (const w of files) push(w, "write");
9028
+ continue;
9029
+ }
9030
+ if (cmd === "dd") {
9031
+ for (const w of rest) if (w.startsWith("of=")) push(w.slice(3), "write");
9032
+ continue;
9033
+ }
9034
+ let operands;
9035
+ if (cmd.includes("-")) {
9036
+ operands = [];
9037
+ const PATH_PARAMS = [
9038
+ "path",
9039
+ "literalpath",
9040
+ "filepath",
9041
+ "destination",
9042
+ "newname",
9043
+ "target"
9044
+ ];
9045
+ const VALUE_PARAMS = /* @__PURE__ */ new Set([
9046
+ ...PATH_PARAMS,
9047
+ "value",
9048
+ "encoding",
9049
+ "filter",
9050
+ "include",
9051
+ "exclude",
9052
+ "itemtype",
9053
+ "name"
9054
+ ]);
9055
+ for (let k = 0; k < rest.length; k++) {
9056
+ const w = rest[k] ?? "";
9057
+ if (w.startsWith("-")) {
9058
+ const name = w.slice(1).toLowerCase();
9059
+ const val = rest[k + 1];
9060
+ if (PATH_PARAMS.includes(name) && val !== void 0) operands.push(val);
9061
+ if (VALUE_PARAMS.has(name) && val !== void 0 && !val.startsWith("-")) k++;
9062
+ continue;
9063
+ }
9064
+ operands.push(w);
9065
+ }
9066
+ } else operands = rule.skipFlags ? rest.filter((w) => !w.startsWith("-") || w === "-") : rest;
9067
+ const op = cmd === "rm" || cmd === "rmdir" || cmd === "unlink" || cmd === "del" || cmd === "erase" || cmd === "remove-item" ? "delete" : "write";
9068
+ if (rule.operands === "last") {
9069
+ const last = operands[operands.length - 1];
9070
+ if (last !== void 0) push(last, op);
9071
+ } else for (const w of operands) push(w, op);
9072
+ }
9073
+ return out;
9074
+ }
8674
9075
  const PATCH_HEADER = /^\*\*\* (Update|Add|Delete) File: (.+?)\s*$/;
8675
9076
  const PATCH_MOVE = /^\*\*\* Move to: (.+?)\s*$/;
8676
9077
  function parseApplyPatch(patch) {
@@ -8715,7 +9116,33 @@ function joinContent(parts) {
8715
9116
  return present.length === 0 ? void 0 : present.join("\n");
8716
9117
  }
8717
9118
  function extractTargets(p) {
8718
- if (!isWriteTool(p.toolName)) return [];
9119
+ const cls = classifyTool(p.toolName);
9120
+ if (cls === "other") return {
9121
+ cls,
9122
+ targets: [],
9123
+ undetermined: false
9124
+ };
9125
+ if (cls === "shell") {
9126
+ const command = firstString(p.input, SHELL_COMMAND_KEYS);
9127
+ if (command === void 0) return {
9128
+ cls,
9129
+ targets: [],
9130
+ undetermined: false
9131
+ };
9132
+ return {
9133
+ cls,
9134
+ targets: extractShellWriteTargets(command),
9135
+ undetermined: false
9136
+ };
9137
+ }
9138
+ const targets = extractWriteTargets(p);
9139
+ return {
9140
+ cls,
9141
+ targets,
9142
+ undetermined: targets.length === 0
9143
+ };
9144
+ }
9145
+ function extractWriteTargets(p) {
8719
9146
  const n = p.toolName.toLowerCase();
8720
9147
  const input = p.input;
8721
9148
  if (n.includes("apply_patch")) {
@@ -8773,9 +9200,9 @@ const PATH_CODES = /* @__PURE__ */ new Set([
8773
9200
  "ERR_PATH_RESERVED_NAME",
8774
9201
  "ERR_PATH_INVALID_CHAR"
8775
9202
  ]);
8776
- async function resolveGateTarget(rootReal, target) {
9203
+ async function resolveGateTarget(rootReal, target, cwdReal = rootReal) {
8777
9204
  const supplied = target.rawPath.replace(/\\/g, "/");
8778
- let abs = path.isAbsolute(supplied) ? path.resolve(supplied) : path.resolve(rootReal, supplied);
9205
+ let abs = path.isAbsolute(supplied) ? path.resolve(supplied) : path.resolve(cwdReal, supplied);
8779
9206
  if (path.isAbsolute(supplied)) abs = await canonicalizeExisting(abs);
8780
9207
  const rel = path.relative(rootReal, abs);
8781
9208
  if (rel === "" || rel === "." || rel.startsWith("..") || path.isAbsolute(rel)) throw new GateDeny("ERR_CONTAINMENT", "target escapes root", supplied);
@@ -8865,7 +9292,8 @@ async function runGatePredicates(targets, profile) {
8865
9292
  const hasContent = targets.some((t) => t.target.content !== void 0);
8866
9293
  if (hasContent && profile.noSecrets) findings.push(...await contentNoSecrets.run(ctx, {
8867
9294
  disable: [],
8868
- allowPaths: []
9295
+ allowPaths: [],
9296
+ pii: profile.pii
8869
9297
  }));
8870
9298
  if (hasContent && profile.maxBytes !== void 0) findings.push(...await contentMaxBytes.run(ctx, { max: profile.maxBytes }));
8871
9299
  const f = findings[0];
@@ -8884,29 +9312,84 @@ const RANK = {
8884
9312
  info: 2,
8885
9313
  debug: 3
8886
9314
  };
8887
- function denyResult(d, stderr, t0) {
9315
+ function denyDocument(d, reason, extra) {
9316
+ return {
9317
+ hookSpecificOutput: {
9318
+ hookEventName: "PreToolUse",
9319
+ permissionDecision: "deny",
9320
+ permissionDecisionReason: reason
9321
+ },
9322
+ permissionDecision: "deny",
9323
+ permissionDecisionReason: reason,
9324
+ axiom: {
9325
+ verdict: "deny",
9326
+ code: d.code,
9327
+ ...d.relPath === void 0 ? {} : { path: d.relPath },
9328
+ ...extra.toolClass === void 0 ? {} : { toolClass: extra.toolClass },
9329
+ standard: "owasp-acs/0.1"
9330
+ }
9331
+ };
9332
+ }
9333
+ function denyResult(d, stderr, t0, extra = {}) {
8888
9334
  const where = d.relPath === void 0 ? "" : ` (${d.relPath})`;
8889
9335
  const reason = `AXIOM GATE DENY ${d.code}: ${d.message}${where}`;
8890
9336
  stderr.push(reason);
8891
9337
  return {
8892
9338
  decision: "deny",
9339
+ verdict: "deny",
8893
9340
  exitCode: 2,
8894
9341
  code: d.code,
8895
9342
  reason: d.message,
8896
9343
  ...d.relPath === void 0 ? {} : { path: d.relPath },
8897
- stdout: JSON.stringify({ hookSpecificOutput: {
8898
- hookEventName: "PreToolUse",
8899
- permissionDecision: "deny",
8900
- permissionDecisionReason: reason
8901
- } }),
9344
+ stdout: JSON.stringify(denyDocument(d, reason, extra)),
8902
9345
  stderr,
8903
- durationMs: performance.now() - t0
8904
- };
9346
+ durationMs: performance.now() - t0,
9347
+ ...extra.toolClass === void 0 ? {} : { toolClass: extra.toolClass },
9348
+ ...extra.root === void 0 ? {} : { root: extra.root }
9349
+ };
9350
+ }
9351
+ const AXIOM_REPO_MARKERS = [
9352
+ "journal",
9353
+ "cas",
9354
+ "applied",
9355
+ "profiles",
9356
+ "trust",
9357
+ "lock",
9358
+ "backup"
9359
+ ];
9360
+ async function discoverRoot(start, home = homedir()) {
9361
+ const homeReal = path.resolve(home);
9362
+ const isHomeOrAbove = (p) => {
9363
+ const a = IS_WIN32 ? p.toLowerCase() : p;
9364
+ const h = IS_WIN32 ? homeReal.toLowerCase() : homeReal;
9365
+ return a === h || h.startsWith(a.endsWith(path.sep) ? a : a + path.sep);
9366
+ };
9367
+ let cur = start;
9368
+ for (let depth = 0; depth < 64; depth++) {
9369
+ if (isHomeOrAbove(cur)) break;
9370
+ try {
9371
+ const st = await lstat(path.join(cur, ".git"));
9372
+ if (st.isDirectory() || st.isFile()) return cur;
9373
+ } catch {}
9374
+ try {
9375
+ const ax = path.join(cur, ".axiom");
9376
+ if ((await lstat(ax)).isDirectory()) for (const m of AXIOM_REPO_MARKERS) try {
9377
+ await lstat(path.join(ax, m));
9378
+ return cur;
9379
+ } catch {}
9380
+ } catch {}
9381
+ const parent = path.dirname(cur);
9382
+ if (parent === cur) break;
9383
+ cur = parent;
9384
+ }
9385
+ return start;
8905
9386
  }
8906
9387
  async function runGate(payloadText, opts = {}) {
8907
9388
  const t0 = performance.now();
8908
9389
  const level = opts.logLevel ?? "warn";
8909
9390
  const stderr = [];
9391
+ let toolClass;
9392
+ let rootUsed;
8910
9393
  const log = (lvl, msg) => {
8911
9394
  if (RANK[lvl] <= RANK[level]) stderr.push(`AXIOM GATE ${lvl.toUpperCase()}: ${msg}`);
8912
9395
  };
@@ -8914,15 +9397,24 @@ async function runGate(payloadText, opts = {}) {
8914
9397
  log("debug", `allow: ${why}`);
8915
9398
  return {
8916
9399
  decision: "allow",
9400
+ verdict: "allow",
8917
9401
  exitCode: 0,
8918
9402
  stderr,
8919
- durationMs: performance.now() - t0
9403
+ durationMs: performance.now() - t0,
9404
+ ...toolClass === void 0 ? {} : { toolClass },
9405
+ ...rootUsed === void 0 ? {} : { root: rootUsed }
8920
9406
  };
8921
9407
  };
9408
+ const deny = (d) => denyResult(d, stderr, t0, {
9409
+ ...toolClass === void 0 ? {} : { toolClass },
9410
+ ...rootUsed === void 0 ? {} : { root: rootUsed }
9411
+ });
8922
9412
  const internal = (why) => {
8923
- if (opts.strict === true) return denyResult(new GateDeny("ERR_INTERNAL", `${why} (--strict)`), stderr, t0);
8924
- log("warn", `${why} — failing open`);
8925
- return allow("internal error");
9413
+ if (opts.failOpen === true) {
9414
+ log("warn", `${why} — failing open (--fail-open)`);
9415
+ return allow("internal error");
9416
+ }
9417
+ return deny(new GateDeny("ERR_INTERNAL", `${why} (fail-closed; pass --fail-open to allow)`));
8926
9418
  };
8927
9419
  if (payloadText === void 0) return internal("no payload on stdin (timeout or empty)");
8928
9420
  let raw;
@@ -8932,26 +9424,61 @@ async function runGate(payloadText, opts = {}) {
8932
9424
  return internal(`payload is not JSON: ${err instanceof Error ? err.message : String(err)}`);
8933
9425
  }
8934
9426
  const payload = normalizePayload(raw);
8935
- if (payload === void 0) return allow("payload carries no tool name");
8936
- const targets = extractTargets(payload);
8937
- if (targets.length === 0) return allow(`tool ${payload.toolName} has no write target`);
9427
+ if (payload === void 0) {
9428
+ log("info", "payload carries no tool name");
9429
+ return allow("payload carries no tool name");
9430
+ }
9431
+ const extracted = extractTargets(payload);
9432
+ toolClass = extracted.cls;
9433
+ if (extracted.cls === "other") return allow(`tool ${payload.toolName} is not a write/shell tool`);
9434
+ if (extracted.cls === "shell" && opts.noShellScan === true) return allow(`tool ${payload.toolName} is a shell tool and --no-shell-scan is set`);
9435
+ if (extracted.undetermined) {
9436
+ if (opts.failOpen === true) {
9437
+ log("warn", `write tool ${payload.toolName} has no recognised path key — allowing (--fail-open)`);
9438
+ return allow("undetermined write target");
9439
+ }
9440
+ return deny(new GateDeny("ERR_UNSUPPORTED_OP", `write tool "${payload.toolName}" carries no recognised path key (${PATH_KEYS.join(", ")}); add it to the gate or pass --fail-open`));
9441
+ }
9442
+ const targets = extracted.targets;
9443
+ if (targets.length === 0) return allow(`tool ${payload.toolName} writes nothing recognisable`);
8938
9444
  try {
8939
9445
  const rootArg = payload.cwd ?? opts.root ?? (opts.cwdFallback ?? (() => process.cwd()))();
8940
9446
  let rootReal = await realpathNative(path.resolve(rootArg));
8941
9447
  if (IS_WIN32 && rootReal.startsWith("\\\\?\\")) rootReal = rootReal.slice(4);
9448
+ const cwdReal = rootReal;
9449
+ if (opts.noRootDiscovery !== true) {
9450
+ const discovered = await discoverRoot(rootReal, opts.home);
9451
+ if (discovered !== rootReal) {
9452
+ log("debug", `root discovered ${rootReal} → ${discovered}`);
9453
+ rootReal = discovered;
9454
+ }
9455
+ }
9456
+ rootUsed = rootReal;
8942
9457
  const { profile, source } = await loadGateProfile({
8943
9458
  root: rootReal,
8944
9459
  ...opts.profilePath === void 0 ? {} : { profilePath: opts.profilePath },
8945
9460
  ...opts.home === void 0 ? {} : { home: opts.home }
8946
9461
  });
8947
- log("debug", `root=${rootReal} profile=${source} targets=${targets.length}`);
9462
+ log("debug", `root=${rootReal} profile=${source} class=${extracted.cls} targets=${targets.length}`);
8948
9463
  const resolved = [];
8949
- for (const t of targets) resolved.push(await resolveGateTarget(rootReal, t));
8950
- const deny = await runGatePredicates(resolved, profile);
8951
- if (deny !== void 0) return denyResult(deny, stderr, t0);
8952
- return allow(`${resolved.length} target(s) passed`);
9464
+ for (const t of targets) try {
9465
+ resolved.push(await resolveGateTarget(rootReal, t, cwdReal));
9466
+ } catch (err) {
9467
+ if (extracted.cls === "shell" && err instanceof GateDeny && err.code !== "ERR_CONTAINMENT") {
9468
+ log("debug", `shell operand ${JSON.stringify(t.rawPath)} skipped: ${err.code}`);
9469
+ continue;
9470
+ }
9471
+ if (extracted.cls === "shell" && err instanceof GateDeny && t.rawPath === ".") {
9472
+ if (profile.deny.length > 0) throw new GateDeny("path.deny", "destructive git command on the whole tree", ".");
9473
+ continue;
9474
+ }
9475
+ throw err;
9476
+ }
9477
+ const verdict = await runGatePredicates(resolved, profile);
9478
+ if (verdict !== void 0) return deny(verdict);
9479
+ return allow(`${resolved.length} target(s) passed (${extracted.cls})`);
8953
9480
  } catch (err) {
8954
- if (err instanceof GateDeny) return denyResult(err, stderr, t0);
9481
+ if (err instanceof GateDeny) return deny(err);
8955
9482
  return internal(err instanceof Error ? err.message : String(err));
8956
9483
  }
8957
9484
  }
@@ -9001,7 +9528,7 @@ function readStdin(stream, { timeoutMs = GATE_STDIN_TIMEOUT_MS, maxBytes = GATE_
9001
9528
  stream.resume();
9002
9529
  });
9003
9530
  }
9004
- const GATE_USAGE = "usage: axiom gate --stdin [--root <dir>] [--profile <file>] [--strict] [--log-level error|warn|info|debug]";
9531
+ const GATE_USAGE = "usage: axiom gate --stdin [--root <dir>] [--profile <file>] [--fail-open] [--no-shell-scan] [--no-root-discovery] [--log-level error|warn|info|debug]";
9005
9532
  async function gateMain(argv, io = {}) {
9006
9533
  const t0 = performance.now();
9007
9534
  let values;
@@ -9013,6 +9540,9 @@ async function gateMain(argv, io = {}) {
9013
9540
  root: { type: "string" },
9014
9541
  profile: { type: "string" },
9015
9542
  strict: { type: "boolean" },
9543
+ "fail-open": { type: "boolean" },
9544
+ "no-shell-scan": { type: "boolean" },
9545
+ "no-root-discovery": { type: "boolean" },
9016
9546
  "log-level": { type: "string" }
9017
9547
  },
9018
9548
  strict: true,
@@ -9021,6 +9551,7 @@ async function gateMain(argv, io = {}) {
9021
9551
  } catch (err) {
9022
9552
  return {
9023
9553
  decision: "deny",
9554
+ verdict: "deny",
9024
9555
  exitCode: 2,
9025
9556
  reason: err instanceof Error ? err.message : String(err),
9026
9557
  stderr: [`AXIOM GATE ERROR: ${err instanceof Error ? err.message : String(err)}`, GATE_USAGE],
@@ -9029,6 +9560,7 @@ async function gateMain(argv, io = {}) {
9029
9560
  }
9030
9561
  if (values.stdin !== true) return {
9031
9562
  decision: "deny",
9563
+ verdict: "deny",
9032
9564
  exitCode: 2,
9033
9565
  reason: "--stdin is required",
9034
9566
  stderr: ["AXIOM GATE ERROR: --stdin is required", GATE_USAGE],
@@ -9041,6 +9573,9 @@ async function gateMain(argv, io = {}) {
9041
9573
  if (values.root !== void 0) opts.root = values.root;
9042
9574
  if (values.profile !== void 0) opts.profilePath = values.profile;
9043
9575
  if (values.strict === true) opts.strict = true;
9576
+ if (values["fail-open"] === true) opts.failOpen = true;
9577
+ if (values["no-shell-scan"] === true) opts.noShellScan = true;
9578
+ if (values["no-root-discovery"] === true) opts.noRootDiscovery = true;
9044
9579
  if (io.cwdFallback !== void 0) opts.cwdFallback = io.cwdFallback;
9045
9580
  if (io.home !== void 0) opts.home = io.home;
9046
9581
  if (read.tooLarge) {