@desplega.ai/agent-swarm 2.4.3 → 2.4.4

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 (150) hide show
  1. package/dist/{acp-adapter-1885dpr7.js → acp-adapter-thq04adh.js} +1 -2
  2. package/dist/{actions-pn76n32q.js → actions-j6ywh6x5.js} +17 -17
  3. package/dist/{amp-adapter-af84rx1d.js → amp-adapter-k8b5xmj8.js} +2 -2
  4. package/dist/{app-6sv8mhzm.js → app-rb7vc02s.js} +3 -3
  5. package/dist/{assistant-sc2tf825.js → assistant-ftta1zjf.js} +18 -18
  6. package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-srp6v6af.js} +1 -1
  7. package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-xns7h5h9.js} +1 -1
  8. package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-3qqrh2mz.js} +2 -2
  9. package/dist/{boot-seeds-37sp626b.js → boot-seeds-9ks8hce7.js} +4 -4
  10. package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-k5ksyres.js} +2 -2
  11. package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-n5a5eqks.js} +1 -1
  12. package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-2j3j4kwn.js} +2 -2
  13. package/dist/{cli-v87dr6e3.js → cli-1d70crsy.js} +1 -1
  14. package/dist/{cli-1ma335fz.js → cli-1ephgwnq.js} +2 -2
  15. package/dist/{cli-4yef8ghv.js → cli-2v045xyf.js} +3 -3
  16. package/dist/{cli-73ezfhha.js → cli-2y91wnbh.js} +10 -5
  17. package/dist/{cli-1eszr343.js → cli-2zqpkvtt.js} +1 -1
  18. package/dist/{cli-e4d78xsv.js → cli-37yb7307.js} +1 -1
  19. package/dist/{cli-8y99kfey.js → cli-3cgngyd8.js} +9 -9
  20. package/dist/{cli-wk28hzqn.js → cli-44159k28.js} +1 -1
  21. package/dist/{cli-qp2m4btv.js → cli-4dg55wsk.js} +2 -2
  22. package/dist/{cli-0dxxwfkt.js → cli-51y09c6p.js} +17 -14
  23. package/dist/{cli-aa3tb29r.js → cli-55qmbp6z.js} +1 -1
  24. package/dist/{cli-mzkwkbdy.js → cli-82d3nkc3.js} +1 -1
  25. package/dist/{cli-sds5zzs8.js → cli-9fhkkpj5.js} +78 -12
  26. package/dist/{cli-vq9gqarq.js → cli-amyhm3gj.js} +1 -1
  27. package/dist/{cli-bxrqvpg5.js → cli-b8k7epj1.js} +5 -5
  28. package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
  29. package/dist/{cli-58vwn100.js → cli-bnz0r3kc.js} +1 -1
  30. package/dist/{cli-6eyncg0y.js → cli-cynvc9fj.js} +34 -6
  31. package/dist/{cli-f0p0f6ja.js → cli-dqb4n629.js} +7 -7
  32. package/dist/{cli-pygdsgh8.js → cli-esxerzg7.js} +1 -1
  33. package/dist/{cli-dm8p2dpf.js → cli-gqvh8d8a.js} +6 -6
  34. package/dist/{cli-p0qerd2r.js → cli-hj3debgw.js} +1 -1
  35. package/dist/{cli-836ehxmt.js → cli-nc35pxb7.js} +2 -2
  36. package/dist/{cli-vcgscaan.js → cli-p20r8w42.js} +8 -6
  37. package/dist/{cli-1xsraagq.js → cli-pccfmtb5.js} +4 -4
  38. package/dist/{cli-s9rr4gr2.js → cli-q6r7t299.js} +1036 -298
  39. package/dist/{cli-tkf4x6ha.js → cli-qkhc5aqy.js} +11 -11
  40. package/dist/{cli-1n9gdg56.js → cli-t7cyef5g.js} +4 -4
  41. package/dist/{cli-038fga5k.js → cli-te6b5wvg.js} +148 -183
  42. package/dist/{cli-kgnkyabk.js → cli-v3yr2gbg.js} +109 -35
  43. package/dist/{cli-h0mgxann.js → cli-vdm20wej.js} +2 -2
  44. package/dist/{cli-zvbb4n0e.js → cli-vq97g1jq.js} +2 -2
  45. package/dist/{cli-gx0j2khs.js → cli-w6nadkh4.js} +3 -3
  46. package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
  47. package/dist/{cli-npfe9t1s.js → cli-xc27fwzg.js} +2 -2
  48. package/dist/{cli-6aen3ezb.js → cli-xj0re5tm.js} +1 -1
  49. package/dist/{cli-aypbg9r2.js → cli-zrk6jxv8.js} +1 -1
  50. package/dist/{cli-1012xb7j.js → cli-zrwpctp4.js} +20 -20
  51. package/dist/cli.js +41 -16
  52. package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-968xwfy7.js} +1 -1
  53. package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-ht4z99fa.js} +1 -1
  54. package/dist/{commands-hd574th6.js → commands-pcymaeg9.js} +2 -2
  55. package/dist/{controller-pz5mcfg4.js → controller-fpjyk46r.js} +2 -2
  56. package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-mwzx1fh6.js} +18 -18
  57. package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-cp4s2e2f.js} +1 -1
  58. package/dist/{db-h06dxp05.js → db-sgd12bt9.js} +3 -3
  59. package/dist/{db-6nf1207q.js → db-xsjg9jqq.js} +1 -1
  60. package/dist/{dispatcher-zthpy4m1.js → dispatcher-x0jfw2c9.js} +16 -16
  61. package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-jrq7ma7h.js} +1 -1
  62. package/dist/{external-channel-x01t7ke6.js → external-channel-8njj4h3b.js} +2 -2
  63. package/dist/{grok-adapter-h21jv594.js → grok-adapter-kkr97v2f.js} +2 -3
  64. package/dist/handlers-0pvee9nw.js +39 -0
  65. package/dist/{heartbeat-zzfksckz.js → heartbeat-wd03zhn0.js} +19 -19
  66. package/dist/{hook-z4zhzj1f.js → hook-f0vghj42.js} +1 -1
  67. package/dist/{hook-3xfqhh2s.js → hook-xzj2ts2p.js} +1 -1
  68. package/dist/{http-q813tmya.js → http-4hwxv62p.js} +255 -117
  69. package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
  70. package/dist/{index-220tk9bw.js → index-97q4xn03.js} +19 -10
  71. package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
  72. package/dist/{index-m0a4ydqj.js → index-gdh8jrdm.js} +16 -16
  73. package/dist/{index-wbbs14c5.js → index-hg0x2cdv.js} +16 -16
  74. package/dist/{index-d0g18bax.js → index-thm0y6a8.js} +19 -19
  75. package/dist/{index-x8xbyn1t.js → index-ww2vznkp.js} +1 -1
  76. package/dist/{keepalive-y5t1my7m.js → keepalive-hrh7xnr9.js} +3 -3
  77. package/dist/{lead-2xwtq66b.js → lead-ee8vazz2.js} +26 -27
  78. package/dist/{maintenance-ybfsaedv.js → maintenance-e8wnwnqx.js} +1 -1
  79. package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-zm0yj4sj.js} +2 -2
  80. package/dist/{onboard-xn6fkkys.js → onboard-dtwcq603.js} +2 -2
  81. package/dist/{otel-impl-e6mn0z13.js → otel-impl-w3fbhpn0.js} +1 -1
  82. package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-j38dehfs.js} +3 -3
  83. package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-ae8yttq4.js} +2 -2
  84. package/dist/{rbac-roles-mb470p4e.js → rbac-roles-ejxx9b35.js} +2 -2
  85. package/dist/{render-v2-g0306nh1.js → render-v2-n16n0phe.js} +3 -3
  86. package/dist/{resume-v315hnq6.js → resume-z0fgjc38.js} +5 -5
  87. package/dist/{session-access-2zg285a1.js → session-access-cv3mq6mc.js} +6 -4
  88. package/dist/{setup-a4mtf02k.js → setup-mafhj3yt.js} +1 -1
  89. package/dist/{work-objects-9fr0fgtn.js → work-objects-8b86mgjp.js} +19 -19
  90. package/dist/{worker-qae22jeb.js → worker-16s75hm5.js} +26 -27
  91. package/dist/worker-token-xf11yyds.js +136 -0
  92. package/openapi.json +295 -12
  93. package/package.json +4 -4
  94. package/src/apps/action-runner.ts +4 -0
  95. package/src/apps/catalog.generated.json +3 -0
  96. package/src/apps/page-validator.ts +8 -2
  97. package/src/be/migrations/232_agent_session_tokens.sql +41 -0
  98. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +10 -1
  99. package/src/be/swarm-config-guard.ts +16 -0
  100. package/src/be/users.ts +169 -12
  101. package/src/cli.tsx +24 -0
  102. package/src/commands/worker-token.ts +164 -0
  103. package/src/http/apps.ts +43 -5
  104. package/src/http/codex-oauth-keep-warm.ts +27 -9
  105. package/src/http/config.ts +5 -3
  106. package/src/http/core.ts +2 -0
  107. package/src/http/session-access.ts +51 -2
  108. package/src/http/sessions.ts +249 -10
  109. package/src/http/tasks.ts +4 -32
  110. package/src/providers/acp-adapter.ts +33 -13
  111. package/src/providers/acp-swarm-events.ts +1 -1
  112. package/src/providers/amp-adapter.ts +1 -1
  113. package/src/providers/codex-oauth/storage.ts +1 -1
  114. package/src/rbac/legacy-policy.ts +3 -1
  115. package/src/rbac/permissions.ts +4 -0
  116. package/src/tests/acp-adapter.test.ts +10 -23
  117. package/src/tests/acp-fake-agent.ts +36 -0
  118. package/src/tests/acp-swarm-events.test.ts +1 -1
  119. package/src/tests/agent-session-tokens.test.ts +452 -0
  120. package/src/tests/apps-action-parent-task.test.ts +295 -0
  121. package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
  122. package/src/tests/config-session-auth.test.ts +14 -0
  123. package/src/tests/docs-index-footprint.test.js +2 -2
  124. package/src/tests/grok-adapter.test.ts +3 -3
  125. package/src/tests/heartbeat-jev-gate.test.ts +23 -0
  126. package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
  127. package/src/tests/rbac-engine.test.ts +1 -0
  128. package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
  129. package/src/tests/script-connections.test.ts +2 -0
  130. package/src/tests/session-route-posture.test.ts +2 -0
  131. package/src/tests/worker-token-cli.test.ts +76 -0
  132. package/src/tests/workflow-executors.test.ts +4 -2
  133. package/src/tests/workflow-system-one-extraction.test.ts +805 -0
  134. package/src/types.ts +2 -1
  135. package/src/utils/request-auth-context.ts +2 -1
  136. package/src/workflows/definition.ts +7 -1
  137. package/src/workflows/engine.ts +18 -12
  138. package/src/workflows/executors/index.ts +1 -0
  139. package/src/workflows/executors/registry.ts +2 -0
  140. package/src/workflows/executors/system-one-decision.ts +56 -255
  141. package/src/workflows/executors/system-one-extract-wire.ts +57 -0
  142. package/src/workflows/executors/system-one-extraction.ts +830 -0
  143. package/src/workflows/executors/system-one-providers.ts +277 -110
  144. package/src/workflows/executors/system-one-transport.ts +264 -0
  145. package/templates/official/lead/HEARTBEAT.md +1 -1
  146. package/templates/skills/apps/SKILL.md +3 -1
  147. package/templates/skills/apps/content.md +3 -1
  148. package/templates/skills/heartbeat-runbook/content.md +3 -3
  149. package/dist/cli-v1sjhn52.js +0 -7
  150. package/dist/handlers-2qjw434w.js +0 -39
package/src/be/users.ts CHANGED
@@ -22,9 +22,10 @@
22
22
  import { createHash, randomBytes, randomUUID } from "node:crypto";
23
23
  import { scheduleOrgDomainRecompute } from "../telemetry-identity";
24
24
  import type { User } from "../types";
25
- import { unregisterVolatileSecret } from "../utils/secret-scrubber";
25
+ import { registerVolatileSecret, unregisterVolatileSecret } from "../utils/secret-scrubber";
26
26
  import { closeSessionTokenConnections } from "../utils/session-token-connections";
27
27
  import { getDbClient } from "./db";
28
+ import type { DbExecutor } from "./db-client";
28
29
  import { validateRuntimeTokenTtlHours } from "./swarm-config-guard";
29
30
 
30
31
  // ---------------------------------------------------------------------------
@@ -547,6 +548,12 @@ export async function resolveUserByToken(plaintext: string): Promise<User | null
547
548
 
548
549
  const SESSION_TOKEN_PREFIX = "aseph_"; // agent-swarm-ephemeral
549
550
 
551
+ /**
552
+ * What an agent-principal session token is bound to: one task (ACP), one
553
+ * runtime instance (runtime pools), or only its agent (a worker token).
554
+ */
555
+ export type SessionTokenKind = "task" | "runtime" | "agent";
556
+
550
557
  export class InvalidRuntimeTokenError extends Error {
551
558
  constructor() {
552
559
  super("Runtime token is no longer valid");
@@ -563,14 +570,21 @@ export async function mintSessionToken(
563
570
  agentId: string,
564
571
  taskId: string,
565
572
  ttlMs: number,
566
- parent?: { tokenId: string; runtimeId: string },
573
+ parent?: { tokenId: string; runtimeId: string | null },
567
574
  ): Promise<{ tokenId: string; plaintext: string }> {
568
575
  if (parent) {
569
576
  return await getDbClient().transaction(async (client) => {
577
+ // A runtime parent is matched by its runtime; an agent parent has no
578
+ // binding but its agent, so it is matched by the empty binding columns.
570
579
  const row = await client.get<{ expiresAt: string }>(
571
- `SELECT expiresAt FROM session_tokens WHERE id = ? AND agentId = ? AND runtimeId = ?
572
- AND revokedAt IS NULL AND expiresAt > ?`,
573
- [parent.tokenId, agentId, parent.runtimeId, new Date().toISOString()],
580
+ parent.runtimeId
581
+ ? `SELECT expiresAt FROM session_tokens WHERE id = ? AND agentId = ? AND runtimeId = ?
582
+ AND revokedAt IS NULL AND expiresAt > ?`
583
+ : `SELECT expiresAt FROM session_tokens WHERE id = ? AND agentId = ? AND ${AGENT_TOKEN_BINDING}
584
+ AND revokedAt IS NULL AND expiresAt > ?`,
585
+ parent.runtimeId
586
+ ? [parent.tokenId, agentId, parent.runtimeId, new Date().toISOString()]
587
+ : [parent.tokenId, agentId, new Date().toISOString()],
574
588
  );
575
589
  if (!row) throw new InvalidRuntimeTokenError();
576
590
  return await insertSessionToken(
@@ -580,6 +594,8 @@ export async function mintSessionToken(
580
594
  ttlMs,
581
595
  parent.runtimeId,
582
596
  row.expiresAt,
597
+ null,
598
+ parent.runtimeId ? null : parent.tokenId,
583
599
  );
584
600
  });
585
601
  }
@@ -607,6 +623,7 @@ export async function insertSessionToken(
607
623
  parentRuntimeId: string | null = null,
608
624
  expiresAtCeiling?: string,
609
625
  controllerId: string | null = null,
626
+ parentTokenId: string | null = null,
610
627
  ): Promise<{ tokenId: string; plaintext: string }> {
611
628
  const plaintext = `${SESSION_TOKEN_PREFIX}${base62(randomBytes(24))}`;
612
629
  const tokenId = randomUUID().replace(/-/g, "");
@@ -619,8 +636,8 @@ export async function insertSessionToken(
619
636
 
620
637
  await getDbClient().run(
621
638
  `INSERT INTO session_tokens
622
- (id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, expiresAt, createdAt, controllerId)
623
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
639
+ (id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, expiresAt, createdAt, controllerId, parentTokenId)
640
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
624
641
  [
625
642
  tokenId,
626
643
  hash,
@@ -632,6 +649,7 @@ export async function insertSessionToken(
632
649
  expiresAt,
633
650
  now.toISOString(),
634
651
  controllerId,
652
+ parentTokenId,
635
653
  ],
636
654
  );
637
655
 
@@ -648,15 +666,31 @@ export async function revokeSessionToken(tokenId: string): Promise<void> {
648
666
  if (row?.runtimeId) {
649
667
  await revokeRuntimeTokensForRuntime(row.runtimeId);
650
668
  } else {
651
- await client.run(
652
- "UPDATE session_tokens SET revokedAt = ? WHERE id = ? AND revokedAt IS NULL",
653
- [new Date().toISOString(), tokenId],
654
- );
655
- getDbClient().afterCommit(() => closeSessionTokenConnections([tokenId]));
669
+ await revokeTokensWithChildren(client, [tokenId]);
656
670
  }
657
671
  });
658
672
  }
659
673
 
674
+ /** Revoke tokens and the child task tokens an agent token minted. */
675
+ async function revokeTokensWithChildren(client: DbExecutor, tokenIds: string[]) {
676
+ if (tokenIds.length === 0) return;
677
+ const placeholders = tokenIds.map(() => "?").join(", ");
678
+ const children = await client.query<{ id: string }>(
679
+ `SELECT id FROM session_tokens WHERE parentTokenId IN (${placeholders})`,
680
+ tokenIds,
681
+ );
682
+ const ids = [...tokenIds, ...children.map((row) => row.id)];
683
+ await client.run(
684
+ `UPDATE session_tokens SET revokedAt = ?
685
+ WHERE id IN (${ids.map(() => "?").join(", ")}) AND revokedAt IS NULL`,
686
+ [new Date().toISOString(), ...ids],
687
+ );
688
+ getDbClient().afterCommit(() => {
689
+ for (const id of tokenIds) unregisterVolatileSecret(`agent-token:${id}`);
690
+ closeSessionTokenConnections(ids);
691
+ });
692
+ }
693
+
660
694
  export async function revokeRuntimeTokensForRuntime(runtimeId: string): Promise<void> {
661
695
  await getDbClient().transaction(async (client) => {
662
696
  const rows = await client.query<{ id: string }>(
@@ -675,6 +709,126 @@ export async function revokeRuntimeTokensForRuntime(runtimeId: string): Promise<
675
709
  });
676
710
  }
677
711
 
712
+ // ---------------------------------------------------------------------------
713
+ // Agent tokens (long-lived, one agent, no runtime)
714
+ // ---------------------------------------------------------------------------
715
+
716
+ /** SQL predicate for an agent token: bound to its agent and nothing else. */
717
+ const AGENT_TOKEN_BINDING =
718
+ "taskId IS NULL AND runtimeId IS NULL AND controllerId IS NULL AND parentRuntimeId IS NULL";
719
+
720
+ /** Stored expiry of a token that does not expire. `expiresAt` is NOT NULL. */
721
+ export const AGENT_TOKEN_NO_EXPIRY = "9999-12-31T23:59:59.999Z";
722
+
723
+ /**
724
+ * Default agent-token lifetime in ms. `null` means the token does not expire
725
+ * and stays valid until it is rotated or revoked. A fixed lifetime (for
726
+ * example 90 days) is a one-line change here.
727
+ */
728
+ export const AGENT_TOKEN_DEFAULT_TTL_MS: number | null = null;
729
+
730
+ export type AgentTokenCredential = {
731
+ tokenId: string;
732
+ plaintext: string;
733
+ /** null when the token does not expire. */
734
+ expiresAt: string | null;
735
+ };
736
+
737
+ export class AgentTokenExistsError extends Error {
738
+ constructor() {
739
+ super("Agent already has an active token; rotate or revoke it");
740
+ }
741
+ }
742
+
743
+ function storedExpiry(ttlMs: number | null): string {
744
+ return ttlMs === null ? AGENT_TOKEN_NO_EXPIRY : new Date(Date.now() + ttlMs).toISOString();
745
+ }
746
+
747
+ function publicExpiry(expiresAt: string): string | null {
748
+ return expiresAt === AGENT_TOKEN_NO_EXPIRY ? null : expiresAt;
749
+ }
750
+
751
+ async function activeAgentTokenIds(client: DbExecutor, agentId: string) {
752
+ const rows = await client.query<{ id: string }>(
753
+ `SELECT id FROM session_tokens WHERE agentId = ? AND ${AGENT_TOKEN_BINDING}
754
+ AND revokedAt IS NULL AND expiresAt > ?`,
755
+ [agentId, new Date().toISOString()],
756
+ );
757
+ return rows.map((row) => row.id);
758
+ }
759
+
760
+ async function insertAgentToken(
761
+ agentId: string,
762
+ ttlMs: number | null,
763
+ ): Promise<AgentTokenCredential> {
764
+ const expiresAt = storedExpiry(ttlMs);
765
+ // Pass the expiry as the ceiling so it is stored exactly; a TTL recomputed
766
+ // from a later clock read could step past AGENT_TOKEN_NO_EXPIRY into year 10000.
767
+ const token = await insertSessionToken(
768
+ agentId,
769
+ null,
770
+ null,
771
+ Number.MAX_SAFE_INTEGER,
772
+ null,
773
+ expiresAt,
774
+ );
775
+ getDbClient().afterCommit(() =>
776
+ registerVolatileSecret(token.plaintext, `agent-token:${token.tokenId}`),
777
+ );
778
+ return { ...token, expiresAt: publicExpiry(expiresAt) };
779
+ }
780
+
781
+ /**
782
+ * Mint the agent-bound worker token for `agentId`. A worker runs on it in
783
+ * place of the swarm API key. Throws `AgentTokenExistsError` when the agent
784
+ * already holds an active one, so tokens do not accumulate silently.
785
+ */
786
+ export async function mintAgentToken(
787
+ agentId: string,
788
+ ttlMs: number | null = AGENT_TOKEN_DEFAULT_TTL_MS,
789
+ ): Promise<AgentTokenCredential> {
790
+ return await getDbClient().transaction(async (client) => {
791
+ if ((await activeAgentTokenIds(client, agentId)).length > 0) {
792
+ throw new AgentTokenExistsError();
793
+ }
794
+ return await insertAgentToken(agentId, ttlMs);
795
+ });
796
+ }
797
+
798
+ /**
799
+ * Replace the agent's active token. The old token keeps working for
800
+ * `graceMs` so a running worker can pick up the new one, then expires. Its
801
+ * child task tokens expire with it, so no credential it delegated outlives
802
+ * it. Returns null when the agent holds no active token.
803
+ */
804
+ export async function rotateAgentToken(
805
+ agentId: string,
806
+ graceMs: number,
807
+ ttlMs: number | null = AGENT_TOKEN_DEFAULT_TTL_MS,
808
+ ): Promise<(AgentTokenCredential & { previousExpiresAt: string }) | null> {
809
+ return await getDbClient().transaction(async (client) => {
810
+ const previous = await activeAgentTokenIds(client, agentId);
811
+ if (previous.length === 0) return null;
812
+ const previousExpiresAt = new Date(Date.now() + graceMs).toISOString();
813
+ const placeholders = previous.map(() => "?").join(", ");
814
+ await client.run(
815
+ `UPDATE session_tokens SET expiresAt = ?
816
+ WHERE (id IN (${placeholders}) OR parentTokenId IN (${placeholders})) AND expiresAt > ?`,
817
+ [previousExpiresAt, ...previous, ...previous, previousExpiresAt],
818
+ );
819
+ return { ...(await insertAgentToken(agentId, ttlMs)), previousExpiresAt };
820
+ });
821
+ }
822
+
823
+ /** Revoke every active agent token of `agentId` and their child task tokens. */
824
+ export async function revokeAgentTokens(agentId: string): Promise<number> {
825
+ return await getDbClient().transaction(async (client) => {
826
+ const ids = await activeAgentTokenIds(client, agentId);
827
+ await revokeTokensWithChildren(client, ids);
828
+ return ids.length;
829
+ });
830
+ }
831
+
678
832
  /**
679
833
  * Resolve a plaintext `aseph_` token to its principal. Returns null when the
680
834
  * token is unknown, revoked, or past its expiry. On a hit, touches `lastUsedAt`
@@ -685,6 +839,7 @@ export async function resolveBySessionToken(
685
839
  options: { touch?: boolean } = {},
686
840
  ): Promise<{
687
841
  tokenId: string;
842
+ tokenKind: SessionTokenKind;
688
843
  agentId: string;
689
844
  taskId: string | null;
690
845
  runtimeId: string | null;
@@ -704,6 +859,7 @@ export async function resolveSessionToken(
704
859
  | {
705
860
  kind: "agent";
706
861
  tokenId: string;
862
+ tokenKind: SessionTokenKind;
707
863
  agentId: string;
708
864
  taskId: string | null;
709
865
  runtimeId: string | null;
@@ -758,6 +914,7 @@ export async function resolveSessionToken(
758
914
  return {
759
915
  kind: "agent",
760
916
  tokenId: row.id,
917
+ tokenKind: row.taskId !== null ? "task" : row.runtimeId !== null ? "runtime" : "agent",
761
918
  agentId: row.agentId,
762
919
  taskId: row.taskId,
763
920
  runtimeId: row.runtimeId,
package/src/cli.tsx CHANGED
@@ -360,6 +360,25 @@ const COMMAND_HELP: Record<
360
360
  ` ${binName} claude-managed-setup --api-url https://swarm.example.com`,
361
361
  ].join("\n"),
362
362
  },
363
+ "worker-token": {
364
+ usage: `${binName} worker-token <create|rotate|revoke> (--agent-id <id> | --name <name>) [options]`,
365
+ description:
366
+ "Manage the agent-bound token a worker runs on in place of the swarm API key. The token acts only as its agent. create and rotate print the new token to stdout once; messages go to stderr. Uses your operator swarm API key.",
367
+ options: [
368
+ " --agent-id <id> Agent the token is bound to",
369
+ " --name <name> Agent name; create makes the agent when no agent has this name",
370
+ " --ttl-days <n> Token lifetime (create, rotate). Default: no expiry",
371
+ " --grace-sec <n> How long the old token keeps working after rotate (default 300)",
372
+ " --api-url <url> Swarm API URL (default: MCP_BASE_URL or http://localhost:3013)",
373
+ " --api-key <key> Operator swarm API key (default: AGENT_SWARM_API_KEY or API_KEY, falling back to 123123)",
374
+ " -h, --help Show this help",
375
+ ].join("\n"),
376
+ examples: [
377
+ ` ${binName} worker-token create --name worker-1`,
378
+ ` ${binName} worker-token rotate --agent-id <agent-id> --grace-sec 600`,
379
+ ` ${binName} worker-token revoke --agent-id <agent-id>`,
380
+ ].join("\n"),
381
+ },
363
382
  e2b: {
364
383
  usage: `${binName} e2b <subcommand> [options]`,
365
384
  description:
@@ -445,6 +464,7 @@ function printHelp(command?: string) {
445
464
  ["docs", "Open documentation (--open to launch in browser)"],
446
465
  ["codex-login", "Authenticate Codex via ChatGPT OAuth"],
447
466
  ["claude-managed-setup", "Bootstrap Anthropic Managed Agents (agent + env + skills)"],
467
+ ["worker-token", "Mint, rotate, or revoke an agent-bound worker token"],
448
468
  ["e2b", "Build templates and start E2B API/worker sandboxes"],
449
469
  ["version", "Show version number"],
450
470
  ["help", "Show this help message"],
@@ -803,6 +823,10 @@ if (args.showHelp || args.command === "help" || args.command === undefined) {
803
823
  const { runClaudeManagedSetup } = await import("./commands/claude-managed-setup");
804
824
  const setupArgs = process.argv.slice(process.argv.indexOf("claude-managed-setup") + 1);
805
825
  await runClaudeManagedSetup(setupArgs);
826
+ } else if (args.command === "worker-token") {
827
+ const { runWorkerToken } = await import("./commands/worker-token");
828
+ const tokenArgs = process.argv.slice(process.argv.indexOf("worker-token") + 1);
829
+ process.exit(await runWorkerToken(tokenArgs));
806
830
  } else if (args.command === "e2b") {
807
831
  const { runE2BCommand } = await import("./commands/e2b");
808
832
  const e2bArgs = process.argv.slice(process.argv.indexOf("e2b") + 1);
@@ -0,0 +1,164 @@
1
+ /**
2
+ * `agent-swarm worker-token create|rotate|revoke`: manage the agent-bound
3
+ * token a worker runs on in place of the swarm API key.
4
+ *
5
+ * Operator-run. It authenticates with the operator's own swarm API key and
6
+ * talks to the API over HTTP only. The new token goes to stdout, once, so it
7
+ * can be captured (`API_KEY=$(agent-swarm worker-token create --name w1)`);
8
+ * everything else goes to stderr.
9
+ */
10
+ import { getApiKey } from "../utils/api-key";
11
+
12
+ type Action = "create" | "rotate" | "revoke";
13
+
14
+ export type WorkerTokenArgs = {
15
+ action?: Action;
16
+ agentId?: string;
17
+ name?: string;
18
+ apiUrl?: string;
19
+ apiKey?: string;
20
+ ttlDays?: number;
21
+ graceSec?: number;
22
+ showHelp: boolean;
23
+ error?: string;
24
+ };
25
+
26
+ export const WORKER_TOKEN_USAGE = `Usage:
27
+ agent-swarm worker-token <create|rotate|revoke> (--agent-id <id> | --name <name>) [options]
28
+
29
+ Options:
30
+ --agent-id <id> Agent the token is bound to
31
+ --name <name> Agent name. create makes the agent when no agent has this name
32
+ --ttl-days <n> Token lifetime (create, rotate). Default: no expiry
33
+ --grace-sec <n> How long the old token keeps working after rotate (default 300)
34
+ --api-url <url> Swarm API URL (default: MCP_BASE_URL or http://localhost:3013)
35
+ --api-key <key> Operator swarm API key (default: AGENT_SWARM_API_KEY or API_KEY, falling back to 123123)
36
+ -h, --help Show this help`;
37
+
38
+ const ACTIONS = new Set<string>(["create", "rotate", "revoke"]);
39
+
40
+ export function parseWorkerTokenArgs(args: string[]): WorkerTokenArgs {
41
+ const parsed: WorkerTokenArgs = { showHelp: false };
42
+ const number = (flag: string, value: string | undefined, min: number) => {
43
+ const n = Number(value);
44
+ if (!Number.isInteger(n) || n < min) parsed.error = `${flag} must be an integer >= ${min}`;
45
+ return n;
46
+ };
47
+ for (let i = 0; i < args.length; i++) {
48
+ const arg = args[i]!;
49
+ if (arg === "--help" || arg === "-h") parsed.showHelp = true;
50
+ else if (arg === "--agent-id") parsed.agentId = args[++i];
51
+ else if (arg === "--name") parsed.name = args[++i];
52
+ else if (arg === "--api-url") parsed.apiUrl = args[++i];
53
+ else if (arg === "--api-key") parsed.apiKey = args[++i];
54
+ else if (arg === "--ttl-days") parsed.ttlDays = number(arg, args[++i], 1);
55
+ else if (arg === "--grace-sec") parsed.graceSec = number(arg, args[++i], 0);
56
+ else if (!parsed.action && ACTIONS.has(arg)) parsed.action = arg as Action;
57
+ else parsed.error = `Unknown argument: ${arg}`;
58
+ }
59
+ if (parsed.error || parsed.showHelp) return parsed;
60
+ if (!parsed.action) parsed.error = "Missing action: create, rotate, or revoke";
61
+ else if (!parsed.agentId === !parsed.name)
62
+ parsed.error = "Pass exactly one of --agent-id or --name";
63
+ return parsed;
64
+ }
65
+
66
+ type Deps = {
67
+ env?: Record<string, string | undefined>;
68
+ fetch?: typeof fetch;
69
+ stdout?: (line: string) => void;
70
+ stderr?: (line: string) => void;
71
+ };
72
+
73
+ async function readError(res: Response): Promise<string> {
74
+ const text = await res.text().catch(() => "");
75
+ try {
76
+ return (JSON.parse(text) as { error?: string }).error ?? text;
77
+ } catch {
78
+ return text;
79
+ }
80
+ }
81
+
82
+ /** Runs the command. Returns the process exit code. */
83
+ export async function runWorkerToken(args: string[], deps: Deps = {}): Promise<number> {
84
+ const env = deps.env ?? process.env;
85
+ const doFetch = deps.fetch ?? fetch;
86
+ const stdout = deps.stdout ?? ((line: string) => console.log(line));
87
+ const stderr = deps.stderr ?? ((line: string) => console.error(line));
88
+ const parsed = parseWorkerTokenArgs(args);
89
+ if (parsed.showHelp) {
90
+ stdout(WORKER_TOKEN_USAGE);
91
+ return 0;
92
+ }
93
+ if (parsed.error) {
94
+ stderr(`${parsed.error}\n\n${WORKER_TOKEN_USAGE}`);
95
+ return 1;
96
+ }
97
+
98
+ const apiUrl = (parsed.apiUrl ?? env.MCP_BASE_URL ?? "http://localhost:3013").replace(/\/+$/, "");
99
+ const apiKey = parsed.apiKey ?? (getApiKey(env) || "123123");
100
+ const headers = { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" };
101
+
102
+ let agentId = parsed.agentId;
103
+ let name: string | undefined;
104
+ if (!agentId) {
105
+ const res = await doFetch(`${apiUrl}/api/agents`, { headers });
106
+ if (!res.ok) {
107
+ stderr(`Listing agents failed (${res.status}): ${await readError(res)}`);
108
+ return 1;
109
+ }
110
+ const { agents } = (await res.json()) as { agents: { id: string; name: string }[] };
111
+ const matches = agents.filter((agent) => agent.name === parsed.name);
112
+ if (matches.length > 1) {
113
+ stderr(`Several agents are named "${parsed.name}"; pass --agent-id instead`);
114
+ return 1;
115
+ }
116
+ agentId = matches[0]?.id;
117
+ if (!agentId) {
118
+ if (parsed.action !== "create") {
119
+ stderr(`No agent is named "${parsed.name}"`);
120
+ return 1;
121
+ }
122
+ agentId = crypto.randomUUID();
123
+ name = parsed.name;
124
+ }
125
+ }
126
+
127
+ const base = `${apiUrl}/api/agents/${encodeURIComponent(agentId)}/token`;
128
+ const ttlMs = parsed.ttlDays === undefined ? undefined : parsed.ttlDays * 86_400_000;
129
+ const res =
130
+ parsed.action === "revoke"
131
+ ? await doFetch(base, { method: "DELETE", headers })
132
+ : await doFetch(parsed.action === "rotate" ? `${base}/rotate` : base, {
133
+ method: "POST",
134
+ headers,
135
+ body: JSON.stringify(
136
+ parsed.action === "rotate" ? { graceSec: parsed.graceSec, ttlMs } : { name, ttlMs },
137
+ ),
138
+ });
139
+ if (!res.ok) {
140
+ stderr(`worker-token ${parsed.action} failed (${res.status}): ${await readError(res)}`);
141
+ return 1;
142
+ }
143
+
144
+ if (parsed.action === "revoke") {
145
+ const { revoked } = (await res.json()) as { revoked: number };
146
+ stderr(`Revoked ${revoked} worker token(s) of agent ${agentId}.`);
147
+ return 0;
148
+ }
149
+ const token = (await res.json()) as {
150
+ plaintext: string;
151
+ expiresAt: string | null;
152
+ agentCreated?: boolean;
153
+ previousExpiresAt?: string;
154
+ };
155
+ if (token.agentCreated) stderr(`Created agent ${agentId} (${name}).`);
156
+ stderr(
157
+ `Worker token for agent ${agentId}, ${token.expiresAt ? `expires ${token.expiresAt}` : "no expiry"}.`,
158
+ );
159
+ if (token.previousExpiresAt)
160
+ stderr(`The previous token stops working at ${token.previousExpiresAt}.`);
161
+ stderr(`Set it as the worker's API_KEY with AGENT_ID=${agentId}. It is shown only once.`);
162
+ stdout(token.plaintext);
163
+ return 0;
164
+ }
package/src/http/apps.ts CHANGED
@@ -62,14 +62,16 @@ import {
62
62
  rollbackApp,
63
63
  snapshotApp,
64
64
  } from "../apps/version";
65
- import { getAgentById, getAppVersion, getAppVersions } from "../be/db";
65
+ import { getAgentById, getAppVersion, getAppVersions, getTaskById } from "../be/db";
66
66
  import type { RbacPrincipal, RbacResource } from "../rbac";
67
67
  import { can } from "../rbac";
68
68
  import { AgentTaskStatusSchema } from "../types";
69
69
  import { getRequestAuth } from "../utils/request-auth-context";
70
70
  import { scrubObject } from "../utils/secret-scrubber";
71
71
  import { resolveHttpFavoriteOwner } from "./favorite-owner";
72
- import { route } from "./route-def";
72
+ import { agentFirstPrincipal } from "./request-principal";
73
+ import { bodyTask, route } from "./route-def";
74
+ import { denyParentAttach } from "./session-access";
73
75
  import { BODY_TOO_LARGE, enforceContentLengthCap, json, jsonError } from "./utils";
74
76
 
75
77
  const MAX_APP_BODY_BYTES = 5 * 1024 * 1024;
@@ -618,17 +620,27 @@ const runActionRoute = route({
618
620
  method: "post",
619
621
  path: "/api/apps/{id}/actions/{name}",
620
622
  pattern: ["api", "apps", null, "actions", null],
623
+ // A task-kind action may chain under `body.parentTaskId`: a user caller needs
624
+ // write access to that session, as for POST /api/tasks.
625
+ session: bodyTask("parentTaskId"),
621
626
  summary: "Run a custom app action",
622
627
  description: "Runs the saved script, or creates the agent task, named by the app definition.",
623
628
  tags: ["Apps"],
624
629
  params: z.object({ id: z.string().min(1), name: AppNameSchema }),
625
- body: z.object({ input: z.record(z.string(), z.unknown()).optional() }),
630
+ body: z.object({
631
+ input: z.record(z.string(), z.unknown()).optional(),
632
+ /** Chains the new task off an existing one. Task-kind actions only: a script or sync action rejects it (400). */
633
+ parentTaskId: z.string().min(1).optional(),
634
+ }),
626
635
  responses: {
627
636
  200: { description: "Action invoked", schema: runActionResultSchema },
628
- 400: { description: "Invalid action input or stale script reference" },
637
+ 400: {
638
+ description:
639
+ "Invalid action input, stale script reference, or parentTaskId on a non-task action",
640
+ },
629
641
  403: { description: "Permission denied" },
630
642
  409: { description: "App definition needs repair" },
631
- 404: { description: "App or action not found" },
643
+ 404: { description: "App, action, or parent task not found" },
632
644
  },
633
645
  rbac: { permission: "app.use" },
634
646
  });
@@ -1382,10 +1394,36 @@ export async function handleApps(
1382
1394
  return true;
1383
1395
  }
1384
1396
  if (definitionNeedsRepair(res, app)) return true;
1397
+ if (parsed.body.parentTaskId) {
1398
+ // `parentTaskId` only means something to a task-kind action: a script or
1399
+ // sync action rejects it rather than silently dropping it. An unknown
1400
+ // action keeps `runAppAction`'s 404.
1401
+ const actions = app.definition.actions;
1402
+ const action =
1403
+ actions && Object.hasOwn(actions, parsed.params.name)
1404
+ ? actions[parsed.params.name]
1405
+ : undefined;
1406
+ if (action && action.kind !== "task") {
1407
+ jsonError(res, "parentTaskId is only supported for task-kind actions", 400);
1408
+ return true;
1409
+ }
1410
+ if (action) {
1411
+ const parentTask = await getTaskById(parsed.body.parentTaskId);
1412
+ if (!parentTask) {
1413
+ jsonError(res, "parent task not found", 404);
1414
+ return true;
1415
+ }
1416
+ // The same gate as POST /api/tasks: without it an app action would
1417
+ // chain under any task past `task.parent.attach`.
1418
+ const principal = await agentFirstPrincipal(req, myAgentId);
1419
+ if (await denyParentAttach(req, res, myAgentId, parentTask, principal)) return true;
1420
+ }
1421
+ }
1385
1422
  const output = await runAppAction({
1386
1423
  app,
1387
1424
  name: parsed.params.name,
1388
1425
  input: parsed.body.input,
1426
+ parentTaskId: parsed.body.parentTaskId,
1389
1427
  caller,
1390
1428
  actor,
1391
1429
  });
@@ -12,9 +12,10 @@
12
12
  * when a task happens to draw it within `REFRESH_SKEW_MS` of expiry — which
13
13
  * for an idle/rarely-drawn slot can be never, letting its refresh token sit
14
14
  * unexercised well past OpenAI's ~8-day session-staleness guidance. This
15
- * sweep calls the SAME `getValidCodexOAuth` with `opts.maxAgeMs` ≈ 7 days so
16
- * every slot refreshes on a roughly-weekly cadence regardless of task draw
17
- * frequency — zero logic duplication, all lock/persist/quarantine behavior
15
+ * sweep calls the SAME `getValidCodexOAuth` with `opts.maxAgeMs` (default 5
16
+ * days, `CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS`) so every slot refreshes on a
17
+ * fixed cadence regardless of task draw frequency — zero logic
18
+ * duplication, all lock/persist/quarantine behavior
18
19
  * reused verbatim (see storage.ts doc comment; hand-rolling a refresher here
19
20
  * would re-create the 2026-06-19 race).
20
21
  *
@@ -25,6 +26,7 @@
25
26
  import type { IncomingMessage, ServerResponse } from "node:http";
26
27
  import { z } from "zod";
27
28
  import { getKv } from "../be/db";
29
+ import { CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS_BOUNDS } from "../be/swarm-config-guard";
28
30
  import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json.js";
29
31
  import { getValidCodexOAuth, loadAllCodexOAuthSlots } from "../providers/codex-oauth/storage.js";
30
32
  import { getApiKey } from "../utils/api-key";
@@ -32,8 +34,25 @@ import { CODEX_AUTH_WATCH_NAMESPACE, codexAuthBenchMarkerKey } from "../utils/co
32
34
  import { getMcpBaseUrl } from "../utils/constants";
33
35
  import { route } from "./route-def";
34
36
 
35
- /** ~weekly refresh cadence, comfortably inside OpenAI's ~8-day staleness window given the 10-day TTL. */
36
- const KEEP_WARM_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
37
+ /**
38
+ * Default refresh age. The old 7-day value lost a slot whose ChatGPT session
39
+ * OpenAI ended at about 6 days 20 hours, before the first attempt. 5 days
40
+ * leaves 44 hours of margin, so even a daily trigger refreshes by 6 days.
41
+ */
42
+ const DEFAULT_KEEP_WARM_MAX_AGE_HOURS = 120;
43
+
44
+ /**
45
+ * Read on every sweep, never at module load, so a stored override applies on
46
+ * config reload without a restart. A non-integer or out-of-range value falls
47
+ * back to the default; the config API rejects writes outside the same bounds.
48
+ */
49
+ export function keepWarmMaxAgeMs(env: NodeJS.ProcessEnv = process.env): number {
50
+ const raw = env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS?.trim();
51
+ const { min, max } = CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS_BOUNDS;
52
+ const value = raw && /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
53
+ const hours = value >= min && value <= max ? value : DEFAULT_KEEP_WARM_MAX_AGE_HOURS;
54
+ return hours * 60 * 60 * 1000;
55
+ }
37
56
 
38
57
  type SlotOutcome =
39
58
  | { slot: number; keySuffix: string; outcome: "warm" | "refreshed" }
@@ -61,7 +80,7 @@ const keepWarmRoute = route({
61
80
  pattern: ["api", "oauth", "keep-warm", "codex"],
62
81
  summary: "Locked keep-warm refresh sweep across all Codex OAuth pool slots",
63
82
  description:
64
- "Enumerates codex_oauth_* slots and refreshes any older than ~7 days through the same locked getValidCodexOAuth path used at task time. Skips slots with a codex-auth-watch bench marker (auth-failure bench or codex-auth-expiry-watch).",
83
+ "Enumerates codex_oauth_* slots and refreshes any older than CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS (default 120 hours) through the same locked getValidCodexOAuth path used at task time. Skips slots with a codex-auth-watch bench marker (auth-failure bench or codex-auth-expiry-watch).",
65
84
  tags: ["OAuth"],
66
85
  responses: {
67
86
  200: {
@@ -82,6 +101,7 @@ export async function handleCodexOAuthKeepWarm(
82
101
 
83
102
  const apiUrl = getMcpBaseUrl();
84
103
  const apiKey = getApiKey();
104
+ const maxAgeMs = keepWarmMaxAgeMs();
85
105
  const slots = await loadAllCodexOAuthSlots(apiUrl, apiKey);
86
106
  const results: SlotOutcome[] = [];
87
107
 
@@ -95,9 +115,7 @@ export async function handleCodexOAuthKeepWarm(
95
115
 
96
116
  try {
97
117
  const beforeAccess = creds.access;
98
- const refreshed = await getValidCodexOAuth(apiUrl, apiKey, slot, {
99
- maxAgeMs: KEEP_WARM_MAX_AGE_MS,
100
- });
118
+ const refreshed = await getValidCodexOAuth(apiUrl, apiKey, slot, { maxAgeMs });
101
119
  if (!refreshed) {
102
120
  results.push({ slot, outcome: "no-credentials" });
103
121
  } else {
@@ -86,7 +86,9 @@ async function requestSecretReadScope(
86
86
  }).allow
87
87
  )
88
88
  return "all";
89
- if (!auth.runtimeId || resolvedScope?.agentId !== auth.agentId) return "none";
89
+ // Runtime and agent tokens stand in for a worker and read their own agent's
90
+ // scope. Task tokens are ACP harness credentials and read no secrets.
91
+ if (auth.tokenKind === "task" || resolvedScope?.agentId !== auth.agentId) return "none";
90
92
  return "self";
91
93
  }
92
94
  if (auth?.kind === "guest") return "none";
@@ -107,7 +109,7 @@ async function resolveSecretsRead(
107
109
  const auth = getRequestAuth(req);
108
110
  return {
109
111
  effectiveIncludeSecrets: includeSecrets && scope !== "none",
110
- includeRepoSecrets: !(auth?.kind === "agent" && auth.runtimeId && scope !== "all"),
112
+ includeRepoSecrets: !(auth?.kind === "agent" && auth.tokenKind !== "task" && scope !== "all"),
111
113
  secretsNote: includeSecrets && scope === "none" ? SECRETS_FORCE_MASK_NOTE : "",
112
114
  };
113
115
  }
@@ -363,7 +365,7 @@ export async function handleConfig(
363
365
  const resolvedScope = {
364
366
  agentId:
365
367
  parsed.query.agentId ||
366
- (auth?.kind === "agent" && auth.runtimeId ? auth.agentId : undefined),
368
+ (auth?.kind === "agent" && auth.tokenKind !== "task" ? auth.agentId : undefined),
367
369
  repoId: parsed.query.repoId || undefined,
368
370
  };
369
371
  const { effectiveIncludeSecrets, includeRepoSecrets, secretsNote } = await resolveSecretsRead(