@astrasyncai/verification-gateway 5.10.0 → 5.12.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 (106) hide show
  1. package/README.md +4 -0
  2. package/dist/adapters/express.d.mts +1 -1
  3. package/dist/adapters/express.d.ts +1 -1
  4. package/dist/adapters/express.js +359 -30
  5. package/dist/adapters/express.js.map +1 -1
  6. package/dist/adapters/express.mjs +359 -30
  7. package/dist/adapters/express.mjs.map +1 -1
  8. package/dist/adapters/mcp.d.mts +5 -1
  9. package/dist/adapters/mcp.d.ts +5 -1
  10. package/dist/adapters/mcp.js +168 -41
  11. package/dist/adapters/mcp.js.map +1 -1
  12. package/dist/adapters/mcp.mjs +168 -41
  13. package/dist/adapters/mcp.mjs.map +1 -1
  14. package/dist/adapters/nextjs.d.mts +1 -1
  15. package/dist/adapters/nextjs.d.ts +1 -1
  16. package/dist/adapters/nextjs.js +352 -13
  17. package/dist/adapters/nextjs.js.map +1 -1
  18. package/dist/adapters/nextjs.mjs +352 -13
  19. package/dist/adapters/nextjs.mjs.map +1 -1
  20. package/dist/adapters/sdk.d.mts +9 -1
  21. package/dist/adapters/sdk.d.ts +9 -1
  22. package/dist/adapters/sdk.js +100 -8
  23. package/dist/adapters/sdk.js.map +1 -1
  24. package/dist/adapters/sdk.mjs +100 -8
  25. package/dist/adapters/sdk.mjs.map +1 -1
  26. package/dist/agent/index.js +1 -1
  27. package/dist/agent/index.js.map +1 -1
  28. package/dist/agent/index.mjs +1 -1
  29. package/dist/agent/index.mjs.map +1 -1
  30. package/dist/bin/astrasync-claude-hook.js +805 -16
  31. package/dist/bin/astrasync-codex-hook.js +805 -16
  32. package/dist/bin/astrasync-guard.js +894 -71
  33. package/dist/bin/astrasync.js +951 -74
  34. package/dist/browser/background.js +284 -7
  35. package/dist/browser/background.js.map +1 -1
  36. package/dist/browser/background.mjs +284 -7
  37. package/dist/browser/background.mjs.map +1 -1
  38. package/dist/cli/index.d.mts +8 -8
  39. package/dist/cli/index.d.ts +8 -8
  40. package/dist/cli/index.js +22 -64
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/cli/index.mjs +22 -64
  43. package/dist/cli/index.mjs.map +1 -1
  44. package/dist/codex/index.d.mts +5 -0
  45. package/dist/codex/index.d.ts +5 -0
  46. package/dist/codex/index.js +821 -24
  47. package/dist/codex/index.js.map +1 -1
  48. package/dist/codex/index.mjs +823 -26
  49. package/dist/codex/index.mjs.map +1 -1
  50. package/dist/cursor/extension.d.mts +9 -2
  51. package/dist/cursor/extension.d.ts +9 -2
  52. package/dist/cursor/extension.js +787 -11
  53. package/dist/cursor/extension.js.map +1 -1
  54. package/dist/cursor/extension.mjs +789 -11
  55. package/dist/cursor/extension.mjs.map +1 -1
  56. package/dist/edge-config.d.mts +1 -1
  57. package/dist/edge-config.d.ts +1 -1
  58. package/dist/edge-config.js +1 -1
  59. package/dist/edge-config.js.map +1 -1
  60. package/dist/edge-config.mjs +1 -1
  61. package/dist/edge-config.mjs.map +1 -1
  62. package/dist/edge-core/index.d.mts +128 -13
  63. package/dist/edge-core/index.d.ts +128 -13
  64. package/dist/edge-core/index.js +337 -55
  65. package/dist/edge-core/index.js.map +1 -1
  66. package/dist/edge-core/index.mjs +334 -55
  67. package/dist/edge-core/index.mjs.map +1 -1
  68. package/dist/enrolment-store-8DyaIOLe.d.mts +26 -0
  69. package/dist/enrolment-store-8DyaIOLe.d.ts +26 -0
  70. package/dist/gateway/gateway.js +284 -7
  71. package/dist/gateway/gateway.js.map +1 -1
  72. package/dist/gateway/gateway.mjs +284 -7
  73. package/dist/gateway/gateway.mjs.map +1 -1
  74. package/dist/git-trigger/git-hooks.d.mts +1 -1
  75. package/dist/git-trigger/git-hooks.d.ts +1 -1
  76. package/dist/index.d.mts +215 -5
  77. package/dist/index.d.ts +215 -5
  78. package/dist/index.js +3756 -80
  79. package/dist/index.js.map +1 -1
  80. package/dist/index.mjs +3748 -80
  81. package/dist/index.mjs.map +1 -1
  82. package/dist/platform-signatures.js +4 -1
  83. package/dist/platform-signatures.js.map +1 -1
  84. package/dist/platform-signatures.mjs +4 -1
  85. package/dist/platform-signatures.mjs.map +1 -1
  86. package/dist/registration/index.js +1 -1
  87. package/dist/registration/index.js.map +1 -1
  88. package/dist/registration/index.mjs +1 -1
  89. package/dist/registration/index.mjs.map +1 -1
  90. package/dist/transport/index.js +1 -1
  91. package/dist/transport/index.js.map +1 -1
  92. package/dist/transport/index.mjs +1 -1
  93. package/dist/transport/index.mjs.map +1 -1
  94. package/dist/{types-CTmUpNpS.d.mts → types-BCkwBA84.d.mts} +12 -0
  95. package/dist/{types-CTmUpNpS.d.ts → types-BCkwBA84.d.ts} +12 -0
  96. package/dist/{types-CtFXmDzm.d.ts → types-C_b4QTyO.d.ts} +18 -0
  97. package/dist/{types-DQrn_kei.d.mts → types-DlshIIR-.d.mts} +18 -0
  98. package/dist/ui/index.d.mts +1 -1
  99. package/dist/ui/index.d.ts +1 -1
  100. package/dist/verify.d.mts +35 -4
  101. package/dist/verify.d.ts +35 -4
  102. package/dist/verify.js +130 -16
  103. package/dist/verify.js.map +1 -1
  104. package/dist/verify.mjs +129 -16
  105. package/dist/verify.mjs.map +1 -1
  106. package/package.json +1 -1
@@ -309,7 +309,8 @@ var CodexAdapter = class {
309
309
  };
310
310
 
311
311
  // src/codex/hook.ts
312
- var import_path4 = require("path");
312
+ var import_os3 = require("os");
313
+ var import_path6 = require("path");
313
314
 
314
315
  // src/gateway/types.ts
315
316
  var HIGH_RISK_COMMANDS = [
@@ -4035,7 +4036,7 @@ function getTrustLevel(score) {
4035
4036
  }
4036
4037
 
4037
4038
  // src/version.ts
4038
- var SDK_VERSION = "5.10.0";
4039
+ var SDK_VERSION = "5.12.0";
4039
4040
 
4040
4041
  // src/http.ts
4041
4042
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -4062,6 +4063,14 @@ function getCachedWellKnownUrls(apiBaseUrl) {
4062
4063
  return cache.get(apiBaseUrl)?.data;
4063
4064
  }
4064
4065
 
4066
+ // src/failure-posture.ts
4067
+ var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
4068
+ "identity.invalid_agent_id",
4069
+ "identity.unknown_agent",
4070
+ "agent.lookup"
4071
+ ]);
4072
+ var UNKNOWN_AGENT_GUIDANCE = "Register your agent with AstraSync (or, inside an MCP host, add the AstraSync MCP connector for keyless, human-approved registration), then present the canonical ASTRA-* id issued after owner approval.";
4073
+
4065
4074
  // src/verify.ts
4066
4075
  var DEFAULT_CONFIG = {
4067
4076
  apiBaseUrl: "https://astrasync.ai/api",
@@ -4273,11 +4282,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
4273
4282
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
4274
4283
  // deterministic and intentionally NOT marked retryable.
4275
4284
  ...isApiError && { retryable: true },
4285
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
4286
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
4276
4287
  correlationId: options.correlationId,
4277
4288
  attemptId: options.attemptId,
4278
4289
  verifiedAt: /* @__PURE__ */ new Date()
4279
4290
  };
4280
4291
  }
4292
+ async function readJsonSafe(response) {
4293
+ try {
4294
+ const parsed = await response.json();
4295
+ return parsed && typeof parsed === "object" ? parsed : {};
4296
+ } catch {
4297
+ return {};
4298
+ }
4299
+ }
4300
+ function rejectedIdentityGuidance(failures) {
4301
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
4302
+ if (!f) return void 0;
4303
+ if (f.dimension === "agent.lookup") {
4304
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
4305
+ }
4306
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
4307
+ }
4308
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
4309
+ function parseRetryAfterMs(response) {
4310
+ const raw = response.headers?.get?.("retry-after");
4311
+ if (!raw) return void 0;
4312
+ const trimmed = raw.trim();
4313
+ let ms;
4314
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
4315
+ ms = Number(trimmed) * 1e3;
4316
+ } else {
4317
+ const at = Date.parse(trimmed);
4318
+ if (Number.isNaN(at)) return void 0;
4319
+ ms = at - Date.now();
4320
+ }
4321
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
4322
+ }
4281
4323
  async function callVerifyAccessAPI(config, request) {
4282
4324
  const { credentials, ...requestData } = request;
4283
4325
  const body = {
@@ -4334,7 +4376,8 @@ async function callVerifyAccessAPI(config, request) {
4334
4376
  body: JSON.stringify(body),
4335
4377
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
4336
4378
  });
4337
- const data = await response.json();
4379
+ const data = await readJsonSafe(response);
4380
+ const retryAfterMs = parseRetryAfterMs(response);
4338
4381
  if (response.status === 410) {
4339
4382
  return {
4340
4383
  success: true,
@@ -4372,6 +4415,23 @@ async function callVerifyAccessAPI(config, request) {
4372
4415
  }
4373
4416
  };
4374
4417
  }
4418
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
4419
+ return {
4420
+ success: true,
4421
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
4422
+ access: {
4423
+ allowed: false,
4424
+ reason: "unknown_agent",
4425
+ failures: [
4426
+ {
4427
+ dimension: "identity.unknown_agent",
4428
+ message: "The presented agent identifier is not registered with AstraSync.",
4429
+ guidance: UNKNOWN_AGENT_GUIDANCE
4430
+ }
4431
+ ]
4432
+ }
4433
+ };
4434
+ }
4375
4435
  if (!response.ok) {
4376
4436
  if (response.status === 401) {
4377
4437
  return {
@@ -4386,9 +4446,11 @@ async function callVerifyAccessAPI(config, request) {
4386
4446
  }
4387
4447
  return {
4388
4448
  success: false,
4389
- error: data.message || data.error || `API returned ${response.status}`,
4449
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
4390
4450
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
4391
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
4451
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
4452
+ httpStatus: response.status,
4453
+ ...retryAfterMs !== void 0 && { retryAfterMs }
4392
4454
  };
4393
4455
  }
4394
4456
  return data;
@@ -4460,12 +4522,14 @@ async function verify(config, request, options) {
4460
4522
  correlationId: apiResponse.correlationId,
4461
4523
  attemptId: apiResponse.attemptId,
4462
4524
  remediation: apiResponse.remediation,
4525
+ httpStatus: apiResponse.httpStatus,
4526
+ retryAfterMs: apiResponse.retryAfterMs,
4463
4527
  urls
4464
4528
  });
4465
4529
  }
4466
4530
  if (!apiResponse.access?.allowed) {
4467
4531
  const aggregatedFailures = apiResponse.access?.failures;
4468
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
4532
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
4469
4533
  const result2 = {
4470
4534
  identityVerified: idVerifiedFromBackend,
4471
4535
  policyAllowed: false,
@@ -4477,7 +4541,10 @@ async function verify(config, request, options) {
4477
4541
  requiresStepUp: apiResponse.access?.requiresStepUp,
4478
4542
  requiresApproval: apiResponse.access?.requiresApproval,
4479
4543
  guidance: {
4480
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
4544
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
4545
+ // actionable registration guidance on its failure — surface it
4546
+ // instead of the bare machine reason ('unknown_agent').
4547
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
4481
4548
  registrationUrl: urls?.registrationUrl ?? "",
4482
4549
  documentationUrl: urls?.documentationUrl ?? "",
4483
4550
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -4577,7 +4644,7 @@ async function verify(config, request, options) {
4577
4644
  registrationUrl: urls?.registrationUrl ?? "",
4578
4645
  documentationUrl: urls?.documentationUrl ?? ""
4579
4646
  };
4580
- } else if (result.recommendation === "step_up_required") {
4647
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
4581
4648
  result.requiresStepUp = true;
4582
4649
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
4583
4650
  }
@@ -4606,6 +4673,217 @@ var DEFAULT_EDGE_CONFIG = Object.freeze({
4606
4673
  var DEFAULT_TTL_MS = 60 * 1e3;
4607
4674
  var DEFAULT_MAX_STALE_MS = 24 * 60 * 60 * 1e3;
4608
4675
 
4676
+ // src/edge-core/events.ts
4677
+ async function withBudget(promise, ms) {
4678
+ let timer;
4679
+ try {
4680
+ return await Promise.race([
4681
+ promise,
4682
+ new Promise((resolve) => {
4683
+ timer = setTimeout(() => resolve(void 0), ms);
4684
+ })
4685
+ ]);
4686
+ } catch {
4687
+ return void 0;
4688
+ } finally {
4689
+ if (timer) clearTimeout(timer);
4690
+ }
4691
+ }
4692
+ var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
4693
+ var BeaconBuffer = class {
4694
+ constructor(options = {}) {
4695
+ this.queue = [];
4696
+ this.droppedSinceLast = 0;
4697
+ this.droppedTotal = 0;
4698
+ this.deliveredTotal = 0;
4699
+ this.backoffUntil = 0;
4700
+ this.collapse = /* @__PURE__ */ new Map();
4701
+ /** Keys the backend answered 401 for — later batches go unauthenticated. */
4702
+ this.rejectedKeys = /* @__PURE__ */ new Set();
4703
+ this.opts = {
4704
+ maxItems: options.maxItems ?? 1e3,
4705
+ maxBatch: options.maxBatch ?? 100,
4706
+ maxAttempts: options.maxAttempts ?? 3,
4707
+ collapseWindowMs: options.collapseWindowMs ?? 6e4,
4708
+ collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
4709
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
4710
+ retryBackoffMs: options.retryBackoffMs ?? 1e3,
4711
+ now: options.now ?? Date.now
4712
+ };
4713
+ }
4714
+ /**
4715
+ * Queue one beacon. With a `collapseKey`, a repeat inside the window is
4716
+ * merged into a held aggregate (sent when the window closes) instead of
4717
+ * becoming its own item.
4718
+ */
4719
+ enqueue(dest, item, collapseKey) {
4720
+ const now = this.opts.now();
4721
+ if (collapseKey) {
4722
+ const entry = this.collapse.get(collapseKey);
4723
+ if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
4724
+ for (const target of [entry.first, entry.held]) {
4725
+ if (target && !target.inFlight && this.queue.includes(target)) {
4726
+ target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
4727
+ return;
4728
+ }
4729
+ }
4730
+ const held = this.push(
4731
+ dest,
4732
+ { ...item, repeatCount: 1 },
4733
+ entry.firstSeenAt + this.opts.collapseWindowMs
4734
+ );
4735
+ entry.held = held;
4736
+ return;
4737
+ }
4738
+ this.collapse.delete(collapseKey);
4739
+ if (this.collapse.size >= this.opts.collapseMaxKeys) {
4740
+ const oldest = this.collapse.keys().next().value;
4741
+ if (oldest !== void 0) this.collapse.delete(oldest);
4742
+ }
4743
+ const fresh = { firstSeenAt: now };
4744
+ this.collapse.set(collapseKey, fresh);
4745
+ fresh.first = this.push(dest, item, now);
4746
+ return;
4747
+ }
4748
+ this.push(dest, item, now);
4749
+ }
4750
+ /**
4751
+ * Deliver every eligible queued item. Concurrent callers share ONE
4752
+ * in-flight drain (it survives the caller's budget and a Lambda freeze),
4753
+ * so nothing in flight is ever sent twice; items queued while it runs are
4754
+ * picked up by the same drain.
4755
+ */
4756
+ flush() {
4757
+ if (!this.inFlight) {
4758
+ this.inFlight = this.drain().finally(() => {
4759
+ this.inFlight = void 0;
4760
+ });
4761
+ }
4762
+ return this.inFlight;
4763
+ }
4764
+ /** Counters for tests and diagnostics. */
4765
+ stats() {
4766
+ return {
4767
+ queueDepth: this.queue.length,
4768
+ droppedSinceLast: this.droppedSinceLast,
4769
+ droppedTotal: this.droppedTotal,
4770
+ deliveredTotal: this.deliveredTotal,
4771
+ flushing: this.inFlight !== void 0
4772
+ };
4773
+ }
4774
+ push(dest, item, eligibleAt) {
4775
+ while (this.queue.length >= this.opts.maxItems) this.dropOldest();
4776
+ const entry = {
4777
+ dest,
4778
+ destKey: destKeyOf(dest),
4779
+ item,
4780
+ attempts: 0,
4781
+ eligibleAt,
4782
+ inFlight: false
4783
+ };
4784
+ this.queue.push(entry);
4785
+ return entry;
4786
+ }
4787
+ dropOldest() {
4788
+ if (this.queue.shift()) this.countDropped(1);
4789
+ }
4790
+ countDropped(n) {
4791
+ this.droppedSinceLast += n;
4792
+ this.droppedTotal += n;
4793
+ }
4794
+ async drain() {
4795
+ for (; ; ) {
4796
+ const now = this.opts.now();
4797
+ if (now < this.backoffUntil) return;
4798
+ const batch = this.takeBatch(now);
4799
+ if (batch.length === 0) return;
4800
+ const ok = await this.send(batch);
4801
+ if (!ok) return;
4802
+ }
4803
+ }
4804
+ /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
4805
+ takeBatch(now) {
4806
+ const first = this.queue.find((q) => q.eligibleAt <= now);
4807
+ if (!first) return [];
4808
+ const batch = [];
4809
+ const rest = [];
4810
+ for (const q of this.queue) {
4811
+ if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
4812
+ q.inFlight = true;
4813
+ batch.push(q);
4814
+ } else {
4815
+ rest.push(q);
4816
+ }
4817
+ }
4818
+ this.queue = rest;
4819
+ return batch;
4820
+ }
4821
+ /** POST one batch. Returns false when the drain should pause (backoff). */
4822
+ async send(batch) {
4823
+ const dest = batch[0].dest;
4824
+ const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
4825
+ const reportedDropped = this.droppedSinceLast;
4826
+ const meta = {
4827
+ droppedSinceLast: reportedDropped,
4828
+ queueDepth: this.queue.length,
4829
+ adapter: dest.adapter ?? "edge-core",
4830
+ version: SDK_VERSION
4831
+ };
4832
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
4833
+ const response = await withBudget(
4834
+ sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
4835
+ method: "POST",
4836
+ headers: {
4837
+ "Content-Type": "application/json",
4838
+ ...useKey && { Authorization: `Bearer ${useKey}` }
4839
+ },
4840
+ body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
4841
+ ...controller && { signal: controller.signal }
4842
+ }),
4843
+ this.opts.requestTimeoutMs
4844
+ );
4845
+ if (!response) controller?.abort();
4846
+ const status = response?.status;
4847
+ for (const q of batch) q.inFlight = false;
4848
+ if (status !== void 0 && status >= 200 && status < 300) {
4849
+ this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
4850
+ this.deliveredTotal += batch.length;
4851
+ return true;
4852
+ }
4853
+ if (status === 401 && useKey) {
4854
+ this.rejectedKeys.add(useKey);
4855
+ this.requeue(batch, false);
4856
+ return true;
4857
+ }
4858
+ if (status === 429) {
4859
+ const retryAfter = Number(response?.headers.get("retry-after"));
4860
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
4861
+ this.backoffUntil = this.opts.now() + waitMs;
4862
+ this.requeue(batch, false);
4863
+ return false;
4864
+ }
4865
+ if (status !== void 0 && status >= 400 && status < 500) {
4866
+ this.countDropped(batch.length);
4867
+ return true;
4868
+ }
4869
+ this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
4870
+ this.requeue(batch, true);
4871
+ return false;
4872
+ }
4873
+ /** Put a failed batch back at the head of the queue (order preserved). */
4874
+ requeue(batch, countAttempt) {
4875
+ const keep = [];
4876
+ for (const q of batch) {
4877
+ if (countAttempt) q.attempts += 1;
4878
+ if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
4879
+ else keep.push(q);
4880
+ }
4881
+ this.queue = [...keep, ...this.queue];
4882
+ while (this.queue.length > this.opts.maxItems) this.dropOldest();
4883
+ }
4884
+ };
4885
+ var beaconBuffer = new BeaconBuffer();
4886
+
4609
4887
  // src/adapters/express.ts
4610
4888
  var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
4611
4889
 
@@ -5200,22 +5478,118 @@ var AstraSyncGateway = class {
5200
5478
  };
5201
5479
 
5202
5480
  // src/claude-code/guard-config.ts
5481
+ var import_fs5 = require("fs");
5482
+ var import_os2 = require("os");
5483
+ var import_path4 = require("path");
5484
+
5485
+ // src/fleet/enrolment-store.ts
5203
5486
  var import_fs4 = require("fs");
5204
5487
  var import_os = require("os");
5205
5488
  var import_path3 = require("path");
5206
- function resolveGuardConfig(env, payloadCwd, home = (0, import_os.homedir)()) {
5489
+ function machineScopeDir(platform = process.platform, env = process.env) {
5490
+ if (platform === "darwin") return "/Library/Application Support/AstraSync";
5491
+ if (platform === "win32") return (0, import_path3.join)(env.ProgramData || "C:\\ProgramData", "AstraSync");
5492
+ return "/etc/astrasync";
5493
+ }
5494
+ function fleetPaths(options = {}) {
5495
+ const home = options.home ?? (0, import_os.homedir)();
5496
+ const machineDir = options.machineDir ?? options.env?.ASTRASYNC_FLEET_MACHINE_DIR ?? machineScopeDir(options.platform, options.env);
5497
+ const userDir = (0, import_path3.join)(home, ".astrasync", "fleet");
5498
+ return {
5499
+ machineDevice: (0, import_path3.join)(machineDir, "fleet.json"),
5500
+ machinePolicy: (0, import_path3.join)(machineDir, "policy.jws"),
5501
+ userDir,
5502
+ userDevice: (0, import_path3.join)(userDir, "device.json"),
5503
+ userPolicy: (0, import_path3.join)(userDir, "policy.jws"),
5504
+ userKeyChain: (0, import_path3.join)(userDir, "keychain.json"),
5505
+ state: (0, import_path3.join)(userDir, "state.json")
5506
+ };
5507
+ }
5508
+ function isEnrolment(value) {
5509
+ const v = value;
5510
+ return !!v && typeof v.deviceId === "string" && typeof v.credential === "string" && typeof v.apiBaseUrl === "string" && !!v.org && typeof v.org.ownerId === "string" && !!v.jwks && Array.isArray(v.jwks.keys);
5511
+ }
5512
+ function loadEnrolment(paths) {
5513
+ const candidates = [
5514
+ [paths.machineDevice, "machine"],
5515
+ [paths.userDevice, "user"]
5516
+ ];
5517
+ for (const [path, scope] of candidates) {
5518
+ if (!(0, import_fs4.existsSync)(path)) continue;
5519
+ try {
5520
+ const parsed = JSON.parse((0, import_fs4.readFileSync)(path, "utf-8"));
5521
+ if (isEnrolment(parsed)) return { enrolment: parsed, scope };
5522
+ } catch {
5523
+ }
5524
+ return { corrupt: true, path };
5525
+ }
5526
+ return null;
5527
+ }
5528
+ function writeFileMode(path, content, mode) {
5529
+ (0, import_fs4.mkdirSync)((0, import_path3.dirname)(path), { recursive: true, mode: 448 });
5530
+ (0, import_fs4.writeFileSync)(path, content, { encoding: "utf-8", mode });
5531
+ try {
5532
+ (0, import_fs4.chmodSync)(path, mode);
5533
+ } catch {
5534
+ }
5535
+ }
5536
+ function readCachedPolicies(paths) {
5537
+ const out = [];
5538
+ for (const p of [paths.userPolicy, paths.machinePolicy]) {
5539
+ try {
5540
+ if ((0, import_fs4.existsSync)(p)) out.push((0, import_fs4.readFileSync)(p, "utf-8").trim());
5541
+ } catch {
5542
+ }
5543
+ }
5544
+ return out.filter(Boolean);
5545
+ }
5546
+ function writeCachedPolicy(paths, jws) {
5547
+ writeFileMode(paths.userPolicy, jws, 384);
5548
+ }
5549
+ function readKeyChain(paths) {
5550
+ try {
5551
+ const parsed = JSON.parse((0, import_fs4.readFileSync)(paths.userKeyChain, "utf-8"));
5552
+ return Array.isArray(parsed) ? parsed.filter((j) => typeof j === "string") : [];
5553
+ } catch {
5554
+ return [];
5555
+ }
5556
+ }
5557
+ function writeKeyChain(paths, chain) {
5558
+ writeFileMode(paths.userKeyChain, JSON.stringify(chain), 384);
5559
+ }
5560
+ function readState(paths) {
5561
+ try {
5562
+ const parsed = JSON.parse((0, import_fs4.readFileSync)(paths.state, "utf-8"));
5563
+ const c = parsed.counts;
5564
+ return {
5565
+ ...parsed,
5566
+ counts: { allow: c?.allow ?? 0, deny: c?.deny ?? 0, ask: c?.ask ?? 0 }
5567
+ };
5568
+ } catch {
5569
+ return { counts: { allow: 0, deny: 0, ask: 0 } };
5570
+ }
5571
+ }
5572
+ function writeState(paths, state) {
5573
+ try {
5574
+ writeFileMode(paths.state, JSON.stringify(state, null, 2), 384);
5575
+ } catch {
5576
+ }
5577
+ }
5578
+
5579
+ // src/claude-code/guard-config.ts
5580
+ function resolveGuardConfig(env, payloadCwd, home = (0, import_os2.homedir)()) {
5207
5581
  const candidateRoots = [env.CLAUDE_PROJECT_DIR, payloadCwd, home].filter(
5208
5582
  (root) => typeof root === "string" && root.length > 0
5209
5583
  );
5210
5584
  for (const root of candidateRoots) {
5211
- const guardDir = (0, import_path3.join)(root, ".astrasync");
5212
- const policyFile = (0, import_path3.join)(guardDir, "policy.yaml");
5213
- if (!(0, import_fs4.existsSync)(policyFile)) continue;
5585
+ const guardDir = (0, import_path4.join)(root, ".astrasync");
5586
+ const policyFile = (0, import_path4.join)(guardDir, "policy.yaml");
5587
+ if (!(0, import_fs5.existsSync)(policyFile)) continue;
5214
5588
  let settings = {};
5215
- const settingsFile = (0, import_path3.join)(guardDir, "guard.json");
5216
- if ((0, import_fs4.existsSync)(settingsFile)) {
5589
+ const settingsFile = (0, import_path4.join)(guardDir, "guard.json");
5590
+ if ((0, import_fs5.existsSync)(settingsFile)) {
5217
5591
  try {
5218
- settings = JSON.parse((0, import_fs4.readFileSync)(settingsFile, "utf-8"));
5592
+ settings = JSON.parse((0, import_fs5.readFileSync)(settingsFile, "utf-8"));
5219
5593
  } catch {
5220
5594
  settings = {};
5221
5595
  }
@@ -5224,6 +5598,408 @@ function resolveGuardConfig(env, payloadCwd, home = (0, import_os.homedir)()) {
5224
5598
  }
5225
5599
  return null;
5226
5600
  }
5601
+ var FLEET_ENROL_INSTRUCTION = "This machine must be enrolled: run `astrasync guard enroll <token>` (token from your organisation admin)";
5602
+ function resolveFleet(env, home = (0, import_os2.homedir)(), paths = fleetPaths({ home, env })) {
5603
+ const loaded = loadEnrolment(paths);
5604
+ if (loaded && "corrupt" in loaded) {
5605
+ return {
5606
+ kind: "fleet-missing",
5607
+ reason: `fleet enrolment at ${loaded.path} is unreadable \u2014 ${FLEET_ENROL_INSTRUCTION}`,
5608
+ paths
5609
+ };
5610
+ }
5611
+ if (loaded) return { kind: "fleet", enrolment: loaded.enrolment, scope: loaded.scope, paths };
5612
+ if (env.ASTRASYNC_FLEET_REQUIRED === "1") {
5613
+ return { kind: "fleet-missing", reason: FLEET_ENROL_INSTRUCTION, paths };
5614
+ }
5615
+ return null;
5616
+ }
5617
+
5618
+ // src/fleet/hook-runner.ts
5619
+ var import_path5 = require("path");
5620
+
5621
+ // src/fleet/client.ts
5622
+ var FleetApiError = class extends Error {
5623
+ constructor(message, status, code) {
5624
+ super(message);
5625
+ this.status = status;
5626
+ this.code = code;
5627
+ this.name = "FleetApiError";
5628
+ }
5629
+ };
5630
+ function base(url) {
5631
+ return url.replace(/\/+$/, "");
5632
+ }
5633
+ async function call(url, init, timeoutMs) {
5634
+ const controller = new AbortController();
5635
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
5636
+ try {
5637
+ const res = await sdkFetch(url, { ...init, signal: controller.signal });
5638
+ const text = await res.text();
5639
+ let body = text;
5640
+ try {
5641
+ body = text ? JSON.parse(text) : null;
5642
+ } catch {
5643
+ }
5644
+ return { status: res.status, body, headers: res.headers };
5645
+ } finally {
5646
+ clearTimeout(timer);
5647
+ }
5648
+ }
5649
+ function apiError(status, body, what) {
5650
+ const b = body ?? {};
5651
+ const code = b.code ?? (typeof b.error === "string" ? b.error : void 0);
5652
+ return new FleetApiError(`${what} failed (${status}${code ? ` ${code}` : ""})`, status, code);
5653
+ }
5654
+ function deviceHeaders(enrolment) {
5655
+ return { Authorization: `Device ${enrolment.credential}` };
5656
+ }
5657
+ function policyEtag(sha256, kid) {
5658
+ return `${sha256}.${kid ?? ""}`;
5659
+ }
5660
+ async function fetchFleetPolicy(enrolment, ifNoneMatch, timeoutMs = 1500) {
5661
+ const headers = deviceHeaders(enrolment);
5662
+ if (ifNoneMatch) headers["If-None-Match"] = `"${ifNoneMatch}"`;
5663
+ const r = await call(`${base(enrolment.apiBaseUrl)}/fleet/policy`, { headers }, timeoutMs);
5664
+ if (r.status === 304) return { status: "unchanged" };
5665
+ if (r.status !== 200) throw apiError(r.status, r.body, "Policy fetch");
5666
+ const data = r.body;
5667
+ if (!data?.policyJws) throw new FleetApiError("Policy response has no policyJws", r.status);
5668
+ return {
5669
+ status: "ok",
5670
+ policyJws: data.policyJws,
5671
+ sha256: data.sha256 ?? "",
5672
+ version: data.version ?? 0,
5673
+ keyChain: Array.isArray(data.keyChain) ? data.keyChain.filter((j) => typeof j === "string") : []
5674
+ };
5675
+ }
5676
+ async function sendHeartbeat(enrolment, request, timeoutMs = 1500) {
5677
+ const r = await call(
5678
+ `${base(enrolment.apiBaseUrl)}/fleet/heartbeat`,
5679
+ {
5680
+ method: "POST",
5681
+ headers: { ...deviceHeaders(enrolment), "Content-Type": "application/json" },
5682
+ body: JSON.stringify(request)
5683
+ },
5684
+ timeoutMs
5685
+ );
5686
+ if (r.status !== 200) throw apiError(r.status, r.body, "Heartbeat");
5687
+ return r.body;
5688
+ }
5689
+
5690
+ // src/fleet/policy-verify.ts
5691
+ var import_crypto = require("crypto");
5692
+ var import_jose2 = require("jose");
5693
+
5694
+ // src/fleet/types.ts
5695
+ var FLEET_POLICY_TYP = "fleet-policy+jwt";
5696
+ var FLEET_POLICY_AUD = "astrasync:fleet-policy";
5697
+ var FLEET_POLICY_ISS = "astrasync.ai";
5698
+ var FLEET_KEYS_TYP = "fleet-keys+jwt";
5699
+ var FLEET_KEYS_AUD = "astrasync:fleet-keys";
5700
+ var DEFAULT_HEARTBEAT_INTERVAL_SEC = 900;
5701
+
5702
+ // src/fleet/policy-verify.ts
5703
+ var FleetPolicyError = class extends Error {
5704
+ constructor(message) {
5705
+ super(message);
5706
+ this.name = "FleetPolicyError";
5707
+ }
5708
+ };
5709
+ function sha256Hex(text) {
5710
+ return (0, import_crypto.createHash)("sha256").update(text, "utf8").digest("hex");
5711
+ }
5712
+ function parseSettings(raw) {
5713
+ if (!raw || typeof raw !== "object") throw new FleetPolicyError("settings missing");
5714
+ const s = raw;
5715
+ if (s.posture !== "active" && s.posture !== "passive") {
5716
+ throw new FleetPolicyError("settings.posture must be active or passive");
5717
+ }
5718
+ if (s.failOpen !== void 0 && typeof s.failOpen !== "boolean") {
5719
+ throw new FleetPolicyError("settings.failOpen must be a boolean");
5720
+ }
5721
+ if (s.governMcp !== void 0 && typeof s.governMcp !== "boolean") {
5722
+ throw new FleetPolicyError("settings.governMcp must be a boolean");
5723
+ }
5724
+ if (s.skipTools !== void 0 && !(Array.isArray(s.skipTools) && s.skipTools.every((t) => typeof t === "string"))) {
5725
+ throw new FleetPolicyError("settings.skipTools must be a string array");
5726
+ }
5727
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
5728
+ return {
5729
+ posture: s.posture,
5730
+ failOpen: s.failOpen === true,
5731
+ ...typeof s.governMcp === "boolean" && { governMcp: s.governMcp },
5732
+ ...Array.isArray(s.skipTools) && { skipTools: s.skipTools },
5733
+ ...num(s.heartbeatIntervalSec) && { heartbeatIntervalSec: num(s.heartbeatIntervalSec) },
5734
+ ...num(s.staleAfterSec) && { staleAfterSec: num(s.staleAfterSec) }
5735
+ };
5736
+ }
5737
+ async function verifyFleetPolicy(jws, jwks, options) {
5738
+ let payloadBytes;
5739
+ let header;
5740
+ try {
5741
+ const keySet = (0, import_jose2.createLocalJWKSet)(jwks);
5742
+ const result = await (0, import_jose2.compactVerify)(jws, keySet, { algorithms: ["ES256"] });
5743
+ payloadBytes = result.payload;
5744
+ header = result.protectedHeader;
5745
+ } catch (err) {
5746
+ throw new FleetPolicyError(
5747
+ `signature verification failed (${err instanceof Error ? err.message : String(err)})`
5748
+ );
5749
+ }
5750
+ if (header.typ !== FLEET_POLICY_TYP) throw new FleetPolicyError(`unexpected typ ${header.typ}`);
5751
+ let claims;
5752
+ try {
5753
+ claims = JSON.parse(new TextDecoder().decode(payloadBytes));
5754
+ } catch {
5755
+ throw new FleetPolicyError("payload is not JSON");
5756
+ }
5757
+ if (claims.aud !== FLEET_POLICY_AUD) throw new FleetPolicyError(`unexpected aud ${claims.aud}`);
5758
+ if (claims.iss !== FLEET_POLICY_ISS) throw new FleetPolicyError(`unexpected iss ${claims.iss}`);
5759
+ if (!options.expectedSubs.includes(claims.sub)) {
5760
+ throw new FleetPolicyError("policy was issued for a different organisation or device");
5761
+ }
5762
+ if (!Number.isInteger(claims.ver) || claims.ver < 1) {
5763
+ throw new FleetPolicyError("ver must be a positive integer");
5764
+ }
5765
+ if (options.minVer !== void 0 && claims.ver < options.minVer) {
5766
+ throw new FleetPolicyError(
5767
+ `policy version ${claims.ver} is older than the accepted version ${options.minVer} (rollback)`
5768
+ );
5769
+ }
5770
+ if (typeof claims.yaml !== "string" || sha256Hex(claims.yaml) !== claims.sha256) {
5771
+ throw new FleetPolicyError("policy hash mismatch");
5772
+ }
5773
+ const settings = parseSettings(claims.settings);
5774
+ const parsed = parseYaml(claims.yaml);
5775
+ if (!parsed.policy) {
5776
+ const first = parsed.validation.errors[0];
5777
+ throw new FleetPolicyError(
5778
+ `signed policy is invalid${first ? ` (${first.path}: ${first.message})` : ""}`
5779
+ );
5780
+ }
5781
+ return { kid: header.kid, claims: { ...claims, settings }, policy: parsed.policy, settings, jws };
5782
+ }
5783
+ async function resolveFleetJwks(pinned, chain, ownerId) {
5784
+ let trusted = pinned;
5785
+ let lastSeq = 0;
5786
+ for (const link of chain) {
5787
+ try {
5788
+ const { payload, protectedHeader } = await (0, import_jose2.compactVerify)(
5789
+ link,
5790
+ (0, import_jose2.createLocalJWKSet)(trusted),
5791
+ { algorithms: ["ES256"] }
5792
+ );
5793
+ if (protectedHeader.typ !== FLEET_KEYS_TYP) continue;
5794
+ const c = JSON.parse(new TextDecoder().decode(payload));
5795
+ if (c.aud !== FLEET_KEYS_AUD || c.iss !== FLEET_POLICY_ISS || c.sub !== ownerId) continue;
5796
+ if (!Number.isInteger(c.seq) || c.seq <= lastSeq) continue;
5797
+ const keys = Array.isArray(c.keys) ? c.keys.filter((k) => k && k.kty === "EC" && k.crv === "P-256" && !("d" in k)) : [];
5798
+ if (keys.length === 0) continue;
5799
+ trusted = { keys };
5800
+ lastSeq = c.seq;
5801
+ } catch {
5802
+ }
5803
+ }
5804
+ return trusted;
5805
+ }
5806
+ async function pickNewestValid(candidates, jwks, options) {
5807
+ let best = null;
5808
+ let lastError = new FleetPolicyError("no cached fleet policy");
5809
+ for (const jws of candidates) {
5810
+ try {
5811
+ const v = await verifyFleetPolicy(jws, jwks, options);
5812
+ if (!best || v.claims.ver > best.claims.ver) best = v;
5813
+ } catch (err) {
5814
+ lastError = err instanceof Error ? err : new FleetPolicyError(String(err));
5815
+ }
5816
+ }
5817
+ if (!best) throw lastError;
5818
+ return best;
5819
+ }
5820
+
5821
+ // src/fleet/runtime.ts
5822
+ var FLEET_SYNC_BUDGET_MS = 1500;
5823
+ function expectedSubs(enrolment) {
5824
+ return [enrolment.org.ownerId, enrolment.deviceId];
5825
+ }
5826
+ function fleetJwks(ctx, chain = readKeyChain(ctx.paths)) {
5827
+ return resolveFleetJwks(ctx.enrolment.jwks, chain, ctx.enrolment.org.ownerId);
5828
+ }
5829
+ async function acceptFetched(ctx, fetched, verifyOpts) {
5830
+ const jwks = await fleetJwks(ctx, fetched.keyChain);
5831
+ const verified = await verifyFleetPolicy(fetched.policyJws, jwks, verifyOpts);
5832
+ if (fetched.keyChain.length > 0) writeKeyChain(ctx.paths, fetched.keyChain);
5833
+ writeCachedPolicy(ctx.paths, fetched.policyJws);
5834
+ return verified;
5835
+ }
5836
+ function remember(paths, state, verified) {
5837
+ state.appliedSha256 = verified.claims.sha256;
5838
+ state.maxVer = Math.max(state.maxVer ?? 0, verified.claims.ver);
5839
+ writeState(paths, state);
5840
+ }
5841
+ async function loadFleetPolicy(ctx, options = {}) {
5842
+ const state = readState(ctx.paths);
5843
+ const verifyOpts = { expectedSubs: expectedSubs(ctx.enrolment), minVer: state.maxVer };
5844
+ const cached = readCachedPolicies(ctx.paths);
5845
+ let cacheError = null;
5846
+ if (cached.length > 0) {
5847
+ try {
5848
+ const verified = await pickNewestValid(cached, await fleetJwks(ctx), verifyOpts);
5849
+ if (verified.claims.sha256 !== state.appliedSha256 || verified.claims.ver !== state.maxVer) {
5850
+ remember(ctx.paths, state, verified);
5851
+ }
5852
+ return verified;
5853
+ } catch (err) {
5854
+ cacheError = err instanceof Error ? err : new FleetPolicyError(String(err));
5855
+ }
5856
+ }
5857
+ try {
5858
+ const fetched = await fetchFleetPolicy(
5859
+ ctx.enrolment,
5860
+ null,
5861
+ options.fetchTimeoutMs ?? FLEET_SYNC_BUDGET_MS
5862
+ );
5863
+ if (fetched.status === "ok") {
5864
+ const verified = await acceptFetched(ctx, fetched, verifyOpts);
5865
+ remember(ctx.paths, state, verified);
5866
+ return verified;
5867
+ }
5868
+ } catch (err) {
5869
+ if (err instanceof FleetApiError && err.code === "DEVICE_REVOKED") {
5870
+ state.revoked = true;
5871
+ writeState(ctx.paths, state);
5872
+ }
5873
+ const detail = err instanceof Error ? err.message : String(err);
5874
+ throw new FleetPolicyError(
5875
+ cacheError ? `${cacheError.message}; refresh failed (${detail})` : `no fleet policy (${detail})`
5876
+ );
5877
+ }
5878
+ throw cacheError ?? new FleetPolicyError("no fleet policy available");
5879
+ }
5880
+ async function recordDecisionAndHeartbeat(ctx, applied, decision, options) {
5881
+ const now = options.now ?? Date.now;
5882
+ const state = readState(ctx.paths);
5883
+ if (decision) state.counts[decision] += 1;
5884
+ const intervalSec = applied.settings.heartbeatIntervalSec ?? ctx.enrolment.heartbeatIntervalSec ?? DEFAULT_HEARTBEAT_INTERVAL_SEC;
5885
+ const last = state.lastHeartbeatAt ? Date.parse(state.lastHeartbeatAt) : 0;
5886
+ if (now() - last < intervalSec * 1e3) {
5887
+ writeState(ctx.paths, state);
5888
+ return "throttled";
5889
+ }
5890
+ const deadline = now() + (options.budgetMs ?? FLEET_SYNC_BUDGET_MS);
5891
+ const remaining = () => Math.max(1, deadline - now());
5892
+ state.lastHeartbeatAt = new Date(now()).toISOString();
5893
+ let outcome = "sent";
5894
+ try {
5895
+ const hb = await sendHeartbeat(
5896
+ ctx.enrolment,
5897
+ {
5898
+ appliedSha256: applied.claims.sha256,
5899
+ guardVersion: options.guardVersion,
5900
+ adapters: [{ kind: options.adapterKind, managed: true }],
5901
+ posture: applied.settings.posture,
5902
+ decisionCounts: { ...state.counts }
5903
+ },
5904
+ remaining()
5905
+ );
5906
+ state.counts = { allow: 0, deny: 0, ask: 0 };
5907
+ state.revoked = false;
5908
+ delete state.lastError;
5909
+ const policyChanged = !!hb.activeSha256 && hb.activeSha256 !== applied.claims.sha256;
5910
+ const keyChanged = !!hb.activeKid && !!applied.kid && hb.activeKid !== applied.kid;
5911
+ if (policyChanged || keyChanged) {
5912
+ const fetched = await fetchFleetPolicy(
5913
+ ctx.enrolment,
5914
+ policyEtag(applied.claims.sha256, applied.kid),
5915
+ remaining()
5916
+ );
5917
+ if (fetched.status === "ok") {
5918
+ const verified = await acceptFetched(ctx, fetched, {
5919
+ expectedSubs: expectedSubs(ctx.enrolment),
5920
+ minVer: Math.max(state.maxVer ?? 0, applied.claims.ver)
5921
+ });
5922
+ state.appliedSha256 = verified.claims.sha256;
5923
+ state.maxVer = Math.max(state.maxVer ?? 0, verified.claims.ver);
5924
+ outcome = "refreshed";
5925
+ }
5926
+ }
5927
+ } catch (err) {
5928
+ if (err instanceof FleetApiError && err.code === "DEVICE_REVOKED") {
5929
+ state.revoked = true;
5930
+ outcome = "revoked";
5931
+ } else {
5932
+ state.lastError = err instanceof Error ? err.message : String(err);
5933
+ outcome = "failed";
5934
+ }
5935
+ }
5936
+ writeState(ctx.paths, state);
5937
+ return outcome;
5938
+ }
5939
+
5940
+ // src/fleet/hook-runner.ts
5941
+ var PREFIX = "AstraSync Local Guard (organisation fleet policy)";
5942
+ async function runFleetHook(args) {
5943
+ if (args.resolution.kind === "fleet-missing") {
5944
+ return { output: args.denyOutput(`${PREFIX}: ${args.resolution.reason}`), exitCode: 0 };
5945
+ }
5946
+ const ctx = { enrolment: args.resolution.enrolment, paths: args.resolution.paths };
5947
+ let verified;
5948
+ try {
5949
+ verified = await loadFleetPolicy(ctx);
5950
+ } catch (err) {
5951
+ const detail = err instanceof Error ? err.message : String(err);
5952
+ return {
5953
+ output: args.denyOutput(`${PREFIX}: no valid signed policy \u2014 ${detail}`),
5954
+ exitCode: 0
5955
+ };
5956
+ }
5957
+ const { settings } = verified;
5958
+ const decide = async (output) => {
5959
+ try {
5960
+ const outcome = await recordDecisionAndHeartbeat(ctx, verified, args.classify(output), {
5961
+ guardVersion: SDK_VERSION,
5962
+ adapterKind: args.adapterKind,
5963
+ now: args.now
5964
+ });
5965
+ args.onHeartbeat?.(outcome);
5966
+ } catch {
5967
+ }
5968
+ return { output, exitCode: 0 };
5969
+ };
5970
+ let payload;
5971
+ try {
5972
+ payload = JSON.parse(args.payloadJson);
5973
+ } catch {
5974
+ if (settings.failOpen) return decide(null);
5975
+ return decide(args.denyOutput(`${PREFIX}: could not parse hook payload`));
5976
+ }
5977
+ try {
5978
+ const gatewayConfig = {
5979
+ mode: "local",
5980
+ policy: verified.policy,
5981
+ posture: settings.posture,
5982
+ traceEnabled: true,
5983
+ tracePath: (0, import_path5.join)(args.resolution.paths.userDir, "traces"),
5984
+ debug: false
5985
+ };
5986
+ const gateway = args.gatewayFactory ? args.gatewayFactory(gatewayConfig) : new AstraSyncGateway(gatewayConfig);
5987
+ await gateway.initialize();
5988
+ const adapter = args.createAdapter({
5989
+ ...settings.skipTools && { skipTools: settings.skipTools },
5990
+ ...typeof settings.governMcp === "boolean" && { governMcp: settings.governMcp }
5991
+ });
5992
+ await adapter.initialize({ gateway, adapterOptions: {} });
5993
+ const output = await adapter.evaluatePayload(payload);
5994
+ await adapter.shutdown();
5995
+ await gateway.shutdown();
5996
+ return decide(output);
5997
+ } catch (err) {
5998
+ if (settings.failOpen) return decide(null);
5999
+ const detail = err instanceof Error ? err.message : String(err);
6000
+ return decide(args.denyOutput(`${PREFIX}: policy evaluation failed (${detail})`));
6001
+ }
6002
+ }
5227
6003
 
5228
6004
  // src/codex/hook.ts
5229
6005
  var MAX_STDIN_BYTES = 1024 * 1024;
@@ -5241,6 +6017,19 @@ function denyOutput(reason) {
5241
6017
  }
5242
6018
  async function processCodexHookPayload(payloadJson, options = {}) {
5243
6019
  const env = options.env ?? process.env;
6020
+ const fleet = resolveFleet(env, options.home ?? (0, import_os3.homedir)(), options.fleetPaths);
6021
+ if (fleet) {
6022
+ return runFleetHook({
6023
+ resolution: fleet,
6024
+ payloadJson,
6025
+ adapterKind: "codex",
6026
+ createAdapter: (adapterOptions) => new CodexAdapter(adapterOptions),
6027
+ denyOutput,
6028
+ classify: (output) => output?.hookSpecificOutput?.permissionDecision === "deny" ? "deny" : output?.hookSpecificOutput?.permissionDecision === "ask" ? "ask" : "allow",
6029
+ gatewayFactory: options.gatewayFactory,
6030
+ now: options.now
6031
+ });
6032
+ }
5244
6033
  if (env.ASTRASYNC_GUARD_DISABLE === "1") {
5245
6034
  return { output: null, exitCode: 0 };
5246
6035
  }
@@ -5273,7 +6062,7 @@ async function processCodexHookPayload(payloadJson, options = {}) {
5273
6062
  policyFile: resolved.policyFile,
5274
6063
  posture: resolved.settings.posture ?? "active",
5275
6064
  traceEnabled: true,
5276
- tracePath: (0, import_path4.join)(resolved.guardDir, "traces"),
6065
+ tracePath: (0, import_path6.join)(resolved.guardDir, "traces"),
5277
6066
  debug: false,
5278
6067
  ...mode === "hybrid" && {
5279
6068
  apiBaseUrl: resolved.settings.apiBaseUrl,
@@ -5348,8 +6137,8 @@ if (typeof require !== "undefined" && require.main === module) {
5348
6137
  }
5349
6138
 
5350
6139
  // src/claude-code/settings-installer.ts
5351
- var import_fs5 = require("fs");
5352
- var import_path5 = require("path");
6140
+ var import_fs6 = require("fs");
6141
+ var import_path7 = require("path");
5353
6142
  var ASTRASYNC_HOOK_MARKER = "astrasync-claude-hook";
5354
6143
  var HOOK_MATCHER = "Bash|PowerShell|Write|Edit|MultiEdit|NotebookEdit|Read|Glob|Grep|WebFetch|WebSearch|Task|Agent|mcp__.*";
5355
6144
  var HOOK_TIMEOUT_SECONDS = 10;
@@ -5359,14 +6148,14 @@ function isGuardEntry(entry, marker2) {
5359
6148
  );
5360
6149
  }
5361
6150
  function readSettings(settingsPath) {
5362
- if (!(0, import_fs5.existsSync)(settingsPath)) return {};
5363
- const content = (0, import_fs5.readFileSync)(settingsPath, "utf-8").trim();
6151
+ if (!(0, import_fs6.existsSync)(settingsPath)) return {};
6152
+ const content = (0, import_fs6.readFileSync)(settingsPath, "utf-8").trim();
5364
6153
  if (!content) return {};
5365
6154
  return JSON.parse(content);
5366
6155
  }
5367
6156
  function writeSettings(settingsPath, settings) {
5368
- (0, import_fs5.mkdirSync)((0, import_path5.dirname)(settingsPath), { recursive: true });
5369
- (0, import_fs5.writeFileSync)(settingsPath, `${JSON.stringify(settings, null, 2)}
6157
+ (0, import_fs6.mkdirSync)((0, import_path7.dirname)(settingsPath), { recursive: true });
6158
+ (0, import_fs6.writeFileSync)(settingsPath, `${JSON.stringify(settings, null, 2)}
5370
6159
  `, "utf-8");
5371
6160
  }
5372
6161
  function buildHookEntry(hookCommand, matcher = HOOK_MATCHER, extraHookFields = {}) {
@@ -5401,7 +6190,7 @@ function installClaudeHooks(settingsPath, hookCommand, marker2 = ASTRASYNC_HOOK_
5401
6190
  return { installed: true, alreadyPresent: false, updated: false };
5402
6191
  }
5403
6192
  function uninstallClaudeHooks(settingsPath, marker2 = ASTRASYNC_HOOK_MARKER) {
5404
- if (!(0, import_fs5.existsSync)(settingsPath)) return false;
6193
+ if (!(0, import_fs6.existsSync)(settingsPath)) return false;
5405
6194
  let settings;
5406
6195
  try {
5407
6196
  settings = readSettings(settingsPath);
@@ -5427,7 +6216,15 @@ var PERMISSION_DENY_PATHS = [
5427
6216
  "~/.claude/settings.json",
5428
6217
  "~/.claude/settings.local.json",
5429
6218
  "**/.astrasync/**",
5430
- "~/.astrasync/**"
6219
+ "~/.astrasync/**",
6220
+ // 5.11.0 fleet mode: enrolment + signed-policy cache (user scope is inside
6221
+ // ~/.astrasync already; listed explicitly so the intent survives edits) and
6222
+ // the machine-scope enrolment dirs. `//` = absolute from filesystem root;
6223
+ // Windows paths are matched in POSIX form (C:\ → /c/).
6224
+ "~/.astrasync/fleet/**",
6225
+ "//etc/astrasync/**",
6226
+ "//Library/Application Support/AstraSync/**",
6227
+ "//c/ProgramData/AstraSync/**"
5431
6228
  ];
5432
6229
  var GUARD_PERMISSION_DENY_RULES = PERMISSION_DENY_PATHS.map(
5433
6230
  (p) => `Edit(${p})`