chainlesschain 0.162.160 → 0.162.161
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/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-DMn528q-.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-D9ZcDCJL.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-A_K-8PX0.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-D4pFW3X6.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-Dz46COAd.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-cbMjbyg0.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-Dtf88GAj.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-C5KRxn52.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-Dd2UK3BN.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-lptKTFdG.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-C2KLtwHz.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-_yspG6rG.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-BGq_GAha.js} +1 -1
- package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-CePor71g.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-CLOWhIT7.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-CivJt2vT.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-Btmo1GGE.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Ct35OlG4.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-CTc2GmhB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-ZtiBETk1.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-BNqMQQXw.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-BmzZd-9V.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-CMVgvP2M.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-CcqQFAfT.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-CTqB1_4V.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-CiKOOzlb.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Wk-OT0gr.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D7xRjPjZ.js → GenericCardRenderer-DbEQT6ue.js} +1 -1
- package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-0ZvCPSD9.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-CmVuKkPb.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CTWE3Jwg.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-CBEIF00L.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-DLhxDwVG.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BgsCBrnc.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-ClBUGDS2.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-CnG_ZnYw.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-BUOAbR1Z.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Dnt_9-WB.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-CLjt4kCD.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-f24U9Aes.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-GrmGQbSq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-DADaWn-n.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-xtf5RX6C.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-Cd27Rgtx.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-fXRxlDjl.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-joCFlNB0.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-B1HoURq0.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-DiwsKbT7.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Xe-BsW6S.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-C23qi8kl.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BS1syX2f.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DRYwwDyh.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BQKaDHPH.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-BC6F9xiG.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-ClFPc1Nd.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-SS9uZKCl.js → Projects-Dn-f5aYe.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-CLWuOUNt.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-DiqULTxb.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-9A03P-E_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-CJZAnbq_.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C9A1R77h.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BIZc6Rky.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-DfIPNtuG.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-C5znFcQ1.js} +2 -2
- package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DSs2z3OX.js} +1 -1
- package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-CV6LcrLY.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-cmhoX0Xj.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-tCsg2po7.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-BK1KUDiy.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-yRgyeA82.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-c1mo1mlp.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-koIAfuT3.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-DkkreAaB.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-fy5Ctf9M.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-Bw70Jf6S.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-BKh6Rx-4.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-Dg54WP0c.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-BvvD5SxJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-B7Ug9x4e.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-BjvuMORY.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-CZqhkx8p.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-ClR50lhX.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-DlfppYjx.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-BirgVr8B.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-B8DP_bax.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-DA8AUZzL.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-C38pLGp1.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-81rtr98o.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-D34ixq8t.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-D4bzqTrZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-9qoVpPt6.js} +1 -1
- package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-B0QArPQt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-B8mkLM4x.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-BIP62a6r.js} +1 -1
- package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-BP-zkXK8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-BS3M5H9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BXd2_LmH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-BlxEphl7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-BqIhLAbA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-BtGxuQrO.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-BvD7b2IK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BvL_3J_n.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeBv896m.js → index-COe_cOLJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-CP5alL4t.js} +1 -1
- package/src/assets/web-panel/assets/index-CY1NURWZ.js +1 -0
- package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-CbhE3j5j.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-Cd3KuNK5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-CoVgZw_b.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-CuS-UPoj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-D0Ih7Hwn.js} +1 -1
- package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-D3zCCZRS.js} +3 -3
- package/src/assets/web-panel/assets/{index-BdyokEau.js → index-D6GfvEQG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-DBFSo1NF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-DFHa1HIg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-DFyWKOBD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-DMgnRwrK.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DUQLXLQM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-D_2VYPr1.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DakfFZ-k.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-Ddk0e_Pi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-DkHqMnn3.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-Dmo3dFHb.js} +1 -1
- package/src/assets/web-panel/assets/index-DokUYqpq.js +1 -0
- package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DvfCEJG-.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-E1OJRXs4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-F76w1NiL.js} +1 -1
- package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-ZRJRItm0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-fI5sXMLg.js} +1 -1
- package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-tKiC8T-4.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-ByyvflIR.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-B3FQm2By.js} +1 -1
- package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-BSfns6Ef.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-CTG1Ieri.js} +1 -1
- package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-DObM_2j6.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-DOfelQ1t.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-CHZILds_.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-REbucKSx.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-C6EZzD7o.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BxkWS-g3.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-Chklyoq6.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-CRVwKrrI.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DV24ik_J.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-C7vIo4Pg.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-By-v5GlF.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-B1w9Am3r.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +15 -1
- package/src/commands/agent.js +7 -0
- package/src/commands/background-session.js +16 -2
- package/src/data/changelog.json +7 -2
- package/src/gateways/ws/background-agent-protocol.js +88 -14
- package/src/lib/agent-authority.js +251 -0
- package/src/lib/background-agent-phase.js +100 -0
- package/src/lib/background-agent-supervisor.js +288 -8
- package/src/lib/backpressure-policy.js +105 -0
- package/src/lib/capability-negotiation.js +212 -0
- package/src/lib/credential-guard.js +11 -0
- package/src/lib/credential-proxy.js +182 -0
- package/src/lib/event-seq-replay.js +167 -0
- package/src/lib/goal-condition-engine.js +369 -0
- package/src/lib/headless-manifest.js +13 -0
- package/src/lib/ide-context.js +53 -14
- package/src/lib/project-instructions.js +98 -1
- package/src/lib/remote-path-mapping.js +229 -0
- package/src/lib/schedule-planner.js +136 -0
- package/src/repl/bg-dashboard.js +13 -12
- package/src/runtime/agent-core.js +11 -4
- package/src/runtime/headless-stream.js +195 -11
- package/src/runtime/system-prompt.js +3 -0
- package/src/workers/background-agent-worker.js +37 -2
- package/src/assets/web-panel/assets/ChatBubbleRenderer-lkavIWMS.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-BqaPmC0W.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
- package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
- package/src/assets/web-panel/assets/index-x7YyhMUa.js +0 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound backpressure policy — bounds server→client buffering when a
|
|
3
|
+
* consumer drains too slowly, so a stuck WS client can't grow the sender's
|
|
4
|
+
* memory without limit (the classic slow-consumer blowup) and the UI degrades
|
|
5
|
+
* explicitly instead of freezing.
|
|
6
|
+
*
|
|
7
|
+
* Pure + deterministic (no transport, no timers): the caller feeds the
|
|
8
|
+
* transport's current queued-byte count (e.g. `ws.bufferedAmount`) and a frame
|
|
9
|
+
* class; the policy decides send-vs-drop with hysteresis and reports the
|
|
10
|
+
* lag-episode transitions the caller turns into a notice.
|
|
11
|
+
*
|
|
12
|
+
* Designed to compose with EventReplayBuffer (event-seq-replay.js): a dropped
|
|
13
|
+
* *droppable* frame should still be RECORDED (it gets a seq and stays in the
|
|
14
|
+
* bounded replay buffer), just not sent. The seq gap on the next *sent* frame
|
|
15
|
+
* then trips the consumer's SeqGapTracker, which requests replaySince — so the
|
|
16
|
+
* dropped frames are recovered once the consumer catches up (or a truncated
|
|
17
|
+
* replay tells it to full-resync). Critical frames are never dropped.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const DEFAULT_HIGH_WATER_BYTES = 4 << 20; // 4 MiB queued → consumer is behind
|
|
21
|
+
const DEFAULT_LOW_WATER_BYTES = 1 << 20; // 1 MiB → considered caught up again
|
|
22
|
+
|
|
23
|
+
/** A frame that must always be delivered (lifecycle, errors, acks). */
|
|
24
|
+
export const CRITICAL = "critical";
|
|
25
|
+
/** A frame safe to drop under pressure and recover via replay/resync. */
|
|
26
|
+
export const DROPPABLE = "droppable";
|
|
27
|
+
|
|
28
|
+
export class BackpressurePolicy {
|
|
29
|
+
constructor({
|
|
30
|
+
highWaterBytes = DEFAULT_HIGH_WATER_BYTES,
|
|
31
|
+
lowWaterBytes = DEFAULT_LOW_WATER_BYTES,
|
|
32
|
+
} = {}) {
|
|
33
|
+
const high = Math.max(
|
|
34
|
+
1,
|
|
35
|
+
Math.floor(highWaterBytes) || DEFAULT_HIGH_WATER_BYTES,
|
|
36
|
+
);
|
|
37
|
+
// Low water must sit below high water or hysteresis collapses to a flapping
|
|
38
|
+
// single threshold; clamp it to at most high-1.
|
|
39
|
+
const low = Math.min(Math.max(0, Math.floor(lowWaterBytes) || 0), high - 1);
|
|
40
|
+
this._high = high;
|
|
41
|
+
this._low = low;
|
|
42
|
+
this._lagging = false;
|
|
43
|
+
this._dropped = 0; // dropped since the CURRENT lag episode started
|
|
44
|
+
this._droppedTotal = 0; // dropped across the whole connection
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True while the consumer is behind (buffered ≥ high, until it drains ≤ low). */
|
|
48
|
+
get lagging() {
|
|
49
|
+
return this._lagging;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Frames dropped across the whole connection. */
|
|
53
|
+
get droppedTotal() {
|
|
54
|
+
return this._droppedTotal;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Decide what to do with one outbound frame.
|
|
59
|
+
* @param {number} bufferedBytes transport's currently-queued bytes
|
|
60
|
+
* (`ws.bufferedAmount`); non-finite → treated as 0 (never lags).
|
|
61
|
+
* @param {"critical"|"droppable"} frameClass
|
|
62
|
+
* @returns {{send:boolean, drop:boolean, lagStarted:boolean, lagEnded:boolean,
|
|
63
|
+
* dropped:number}} `dropped` = count dropped in the episode that just
|
|
64
|
+
* started/ended (for the caller's notice); `send:false` only ever for a
|
|
65
|
+
* droppable frame while lagging.
|
|
66
|
+
*/
|
|
67
|
+
evaluate(bufferedBytes, frameClass) {
|
|
68
|
+
const b = Number.isFinite(bufferedBytes) ? bufferedBytes : 0;
|
|
69
|
+
let lagStarted = false;
|
|
70
|
+
let lagEnded = false;
|
|
71
|
+
|
|
72
|
+
if (!this._lagging && b >= this._high) {
|
|
73
|
+
this._lagging = true;
|
|
74
|
+
this._dropped = 0;
|
|
75
|
+
lagStarted = true;
|
|
76
|
+
} else if (this._lagging && b <= this._low) {
|
|
77
|
+
this._lagging = false;
|
|
78
|
+
lagEnded = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// While lagging, shed droppable frames to let the buffer drain; critical
|
|
82
|
+
// frames (and everything while not lagging) always go out.
|
|
83
|
+
if (this._lagging && frameClass === DROPPABLE) {
|
|
84
|
+
this._dropped += 1;
|
|
85
|
+
this._droppedTotal += 1;
|
|
86
|
+
return {
|
|
87
|
+
send: false,
|
|
88
|
+
drop: true,
|
|
89
|
+
lagStarted,
|
|
90
|
+
lagEnded,
|
|
91
|
+
dropped: this._dropped,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// `dropped` carries the episode's running count so a lag-end notice can
|
|
96
|
+
// report how many droppable frames were shed.
|
|
97
|
+
return {
|
|
98
|
+
send: true,
|
|
99
|
+
drop: false,
|
|
100
|
+
lagStarted,
|
|
101
|
+
lagEnded,
|
|
102
|
+
dropped: this._dropped,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bidirectional capability negotiation with N / N-1 downgrade for Agent
|
|
3
|
+
* Protocol (agent-sdk docs/PROTOCOL.md §1.3).
|
|
4
|
+
*
|
|
5
|
+
* The CLI advertises what it can speak one-directionally (buildAgentCapabilities
|
|
6
|
+
* → `cc agent --capabilities`, and the `system/init` line's protocol_version).
|
|
7
|
+
* That is not negotiation: a client (VS Code / JetBrains panel) had no way to
|
|
8
|
+
* announce what IT understands, and the CLI had no rule to pick a common level
|
|
9
|
+
* or step DOWN when the two disagree. When the protocol later grows a v2 line
|
|
10
|
+
* shape, a v1-only client would silently mis-parse it.
|
|
11
|
+
*
|
|
12
|
+
* This module is the missing algorithm. A client MAY send a `hello` as its
|
|
13
|
+
* first stream-json input line:
|
|
14
|
+
* {"type":"hello","protocol_version":2,"min_protocol_version":1,
|
|
15
|
+
* "features":["event_seq","trace_id"]}
|
|
16
|
+
* The CLI negotiates it against its own offer and echoes the agreed level as a
|
|
17
|
+
* `system/negotiated` line, then honors it for the rest of the run.
|
|
18
|
+
*
|
|
19
|
+
* Rules:
|
|
20
|
+
* - agreedVersion = min(server.max, client.max) — never above what either
|
|
21
|
+
* can parse.
|
|
22
|
+
* - Incompatible when agreedVersion < max(server.min, client.min): the two
|
|
23
|
+
* version ranges don't overlap. ok:false — the caller keeps a safe baseline
|
|
24
|
+
* or errors out; it never speaks a version the peer can't read.
|
|
25
|
+
* - effective features = the server-offered features the client also accepts
|
|
26
|
+
* (intersection), minus any whose minimum protocol version is above the
|
|
27
|
+
* agreed version (an N-only field is dropped for an N-1 session).
|
|
28
|
+
* - No client offer at all → legacy client that predates negotiation. The
|
|
29
|
+
* additive fields are defined "consumers MUST tolerate absence AND
|
|
30
|
+
* presence" (docs/PROTOCOL.md §1.2.1), so we keep FULL behavior unchanged
|
|
31
|
+
* (byte-for-byte) and mark clientAware:false. Negotiation only ever
|
|
32
|
+
* RESTRICTS when a client explicitly narrows the set.
|
|
33
|
+
*
|
|
34
|
+
* Pure: no fs / process / clock. The same algorithm is mirrored in the Java
|
|
35
|
+
* twin (jetbrains-plugin CapabilityNegotiation.java); both read the shared
|
|
36
|
+
* fixture __tests__/fixtures/capability-negotiation-cases.json.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** The current protocol version the CLI speaks (mirror of PROTOCOL_VERSION). */
|
|
40
|
+
export const PROTOCOL_VERSION = 1;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The oldest protocol version the CLI can still speak end-to-end (the N-1 in
|
|
44
|
+
* "N / N-1"). At v1 there is no older line shape, so min === current; once v2
|
|
45
|
+
* ships this drops to 1 so a v1-only client negotiates a v1 session.
|
|
46
|
+
*/
|
|
47
|
+
export const PROTOCOL_MIN_VERSION = 1;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The wire-protocol features subject to negotiation — the additive per-line
|
|
51
|
+
* fields a client may or may not understand. Runtime capabilities (bare,
|
|
52
|
+
* worktree, mcp, …) are NOT negotiated: they change what the CLI can do, not
|
|
53
|
+
* the shape of a line the client must parse.
|
|
54
|
+
*/
|
|
55
|
+
export const PROTOCOL_FEATURES = ["event_seq", "tool_use_id", "trace_id"];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Minimum protocol version a feature requires. Empty today (every current
|
|
59
|
+
* feature is a v1 additive field). When a v2 field lands, add it here so an
|
|
60
|
+
* N-1 (v1) session drops it automatically.
|
|
61
|
+
* @type {Record<string, number>}
|
|
62
|
+
*/
|
|
63
|
+
export const FEATURE_MIN_VERSION = {};
|
|
64
|
+
|
|
65
|
+
function intOr(value, fallback) {
|
|
66
|
+
const n = Number(value);
|
|
67
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Coerce a features declaration into a sorted, de-duplicated array of keys.
|
|
72
|
+
* Accepts an array of strings, or an object whose truthy keys are the features
|
|
73
|
+
* (the nested manifest shape). Anything else → [].
|
|
74
|
+
* @returns {string[]}
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeFeatureList(input) {
|
|
77
|
+
let keys;
|
|
78
|
+
if (Array.isArray(input)) {
|
|
79
|
+
keys = input.filter((k) => typeof k === "string" && k);
|
|
80
|
+
} else if (input && typeof input === "object") {
|
|
81
|
+
keys = Object.keys(input).filter((k) => input[k]);
|
|
82
|
+
} else {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return [...new Set(keys)].sort();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extract the server's negotiation offer from a `cc agent --capabilities`
|
|
90
|
+
* manifest (buildAgentCapabilities output). Only the negotiable wire features
|
|
91
|
+
* the manifest advertises truthy are offered.
|
|
92
|
+
* @param {object} manifest
|
|
93
|
+
* @returns {{protocolVersion:number, minProtocolVersion:number, features:string[]}}
|
|
94
|
+
*/
|
|
95
|
+
export function buildServerOffer(manifest = {}) {
|
|
96
|
+
const protocolVersion = intOr(manifest.protocol_version, PROTOCOL_VERSION);
|
|
97
|
+
const minProtocolVersion = intOr(
|
|
98
|
+
manifest.min_protocol_version,
|
|
99
|
+
Math.min(PROTOCOL_MIN_VERSION, protocolVersion),
|
|
100
|
+
);
|
|
101
|
+
const advertised = normalizeFeatureList(manifest.features);
|
|
102
|
+
const features = PROTOCOL_FEATURES.filter((f) => advertised.includes(f));
|
|
103
|
+
return { protocolVersion, minProtocolVersion, features };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Negotiate the effective protocol version + feature set between a server offer
|
|
108
|
+
* and a client offer.
|
|
109
|
+
*
|
|
110
|
+
* @param {{protocolVersion:number, minProtocolVersion?:number, features:string[]}} server
|
|
111
|
+
* @param {null|{protocolVersion?:number, minProtocolVersion?:number, features?:string[]}} clientOffer
|
|
112
|
+
* @param {object} [opts]
|
|
113
|
+
* @param {Record<string,number>} [opts.featureMinVersion=FEATURE_MIN_VERSION]
|
|
114
|
+
* @returns {{
|
|
115
|
+
* ok:boolean, agreedVersion:(number|null), features:string[],
|
|
116
|
+
* downgraded:boolean, disabledFeatures:string[], clientAware:boolean,
|
|
117
|
+
* reason:(string|null)
|
|
118
|
+
* }}
|
|
119
|
+
*/
|
|
120
|
+
export function negotiateProtocol(server = {}, clientOffer = null, opts = {}) {
|
|
121
|
+
const featureMinVersion = opts.featureMinVersion || FEATURE_MIN_VERSION;
|
|
122
|
+
const serverMax = intOr(server.protocolVersion, PROTOCOL_VERSION);
|
|
123
|
+
const serverMin = intOr(server.minProtocolVersion, serverMax);
|
|
124
|
+
const serverFeatures = normalizeFeatureList(server.features);
|
|
125
|
+
|
|
126
|
+
const versionOk = (f, v) => (featureMinVersion[f] || 0) <= v;
|
|
127
|
+
|
|
128
|
+
// No client offer → legacy peer. Keep full behavior (only version-gate the
|
|
129
|
+
// server's own features against its own max), byte-for-byte unchanged.
|
|
130
|
+
if (clientOffer == null) {
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
agreedVersion: serverMax,
|
|
134
|
+
features: serverFeatures.filter((f) => versionOk(f, serverMax)),
|
|
135
|
+
downgraded: false,
|
|
136
|
+
disabledFeatures: [],
|
|
137
|
+
clientAware: false,
|
|
138
|
+
reason: null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const clientMax = intOr(clientOffer.protocolVersion, serverMax);
|
|
143
|
+
const clientMin = intOr(clientOffer.minProtocolVersion, clientMax);
|
|
144
|
+
const agreedVersion = Math.min(serverMax, clientMax);
|
|
145
|
+
const floor = Math.max(serverMin, clientMin);
|
|
146
|
+
|
|
147
|
+
if (agreedVersion < floor) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
agreedVersion: null,
|
|
151
|
+
features: [],
|
|
152
|
+
downgraded: true,
|
|
153
|
+
disabledFeatures: [...serverFeatures].sort(),
|
|
154
|
+
clientAware: true,
|
|
155
|
+
reason:
|
|
156
|
+
`no common protocol version (server ${serverMin}-${serverMax}, ` +
|
|
157
|
+
`client ${clientMin}-${clientMax})`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// A client that omits `features` accepts whatever the agreed version offers;
|
|
162
|
+
// a client that sends the array narrows to the intersection.
|
|
163
|
+
const clientFeatures =
|
|
164
|
+
clientOffer.features === undefined
|
|
165
|
+
? null
|
|
166
|
+
: new Set(normalizeFeatureList(clientOffer.features));
|
|
167
|
+
|
|
168
|
+
const enabled = [];
|
|
169
|
+
const disabled = [];
|
|
170
|
+
for (const f of serverFeatures) {
|
|
171
|
+
const okVersion = versionOk(f, agreedVersion);
|
|
172
|
+
const okClient = clientFeatures == null || clientFeatures.has(f);
|
|
173
|
+
if (okVersion && okClient) enabled.push(f);
|
|
174
|
+
else disabled.push(f);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
agreedVersion,
|
|
180
|
+
features: enabled.sort(),
|
|
181
|
+
downgraded: agreedVersion < serverMax || disabled.length > 0,
|
|
182
|
+
disabledFeatures: disabled.sort(),
|
|
183
|
+
clientAware: true,
|
|
184
|
+
reason: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Feature key → the stream line field it gates. */
|
|
189
|
+
const FEATURE_TO_FIELD = {
|
|
190
|
+
event_seq: "seq",
|
|
191
|
+
trace_id: "trace_id",
|
|
192
|
+
tool_use_id: "tool_use_id",
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Fold a negotiation result into a live field-gate the emitter reads per line:
|
|
197
|
+
* a field stays stamped only if its feature survived negotiation. On an
|
|
198
|
+
* incompatible (ok:false) result nothing is changed (the caller keeps its safe
|
|
199
|
+
* baseline). Mutates `gate` in place and returns it.
|
|
200
|
+
*
|
|
201
|
+
* @param {{features:string[], ok:boolean}} result
|
|
202
|
+
* @param {{seq?:boolean, trace_id?:boolean, tool_use_id?:boolean}} gate
|
|
203
|
+
*/
|
|
204
|
+
export function applyNegotiationToGate(result, gate = {}) {
|
|
205
|
+
if (!result || result.ok === false) return gate;
|
|
206
|
+
const enabled = new Set(result.features || []);
|
|
207
|
+
for (const feature of PROTOCOL_FEATURES) {
|
|
208
|
+
const field = FEATURE_TO_FIELD[feature];
|
|
209
|
+
if (field) gate[field] = enabled.has(feature);
|
|
210
|
+
}
|
|
211
|
+
return gate;
|
|
212
|
+
}
|
|
@@ -213,6 +213,17 @@ const PRINT_COMMANDS = new Set([
|
|
|
213
213
|
const SECRET_VAR_RE =
|
|
214
214
|
/(?:^|_)(KEY|KEYS|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|PASSPHRASE|CREDENTIAL|CREDENTIALS|PRIVATE|APIKEY|ACCESSKEY)(?:_|$)/i;
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* True when an environment-variable NAME looks like it holds a secret
|
|
218
|
+
* (ANTHROPIC_API_KEY, GITHUB_TOKEN, DB_PASSWORD, …). Shared single source of
|
|
219
|
+
* truth so the credential READ guard and the credential PROXY
|
|
220
|
+
* ([[credential-proxy.js]] — which keeps secrets out of subprocess envs)
|
|
221
|
+
* classify identically. `MONKEY`/`KEYBOARD`/`TOKENIZER` do NOT match.
|
|
222
|
+
*/
|
|
223
|
+
export function isSecretEnvName(name) {
|
|
224
|
+
return typeof name === "string" && SECRET_VAR_RE.test(name);
|
|
225
|
+
}
|
|
226
|
+
|
|
216
227
|
// Ways a secret var is referenced inside a shell segment.
|
|
217
228
|
const SECRET_REF_PATTERNS = [
|
|
218
229
|
/\$env:([A-Za-z_][A-Za-z0-9_]*)/gi, // PowerShell $env:NAME
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential proxy — keep the agent's real, long-lived credentials out of the
|
|
3
|
+
* environment that run_shell / run_code / hook / plugin subprocesses inherit
|
|
4
|
+
* (P0 §"跨平台沙箱与凭据代理").
|
|
5
|
+
*
|
|
6
|
+
* By default a spawned command inherits the WHOLE parent environment — including
|
|
7
|
+
* the agent's ANTHROPIC_API_KEY, cloud keys and tokens. A compromised or
|
|
8
|
+
* careless command can then echo, log, or exfiltrate them. This module replaces
|
|
9
|
+
* credential-named vars with an opaque SENTINEL before the child starts and
|
|
10
|
+
* keeps the real values in a parent-held vault, to be injected only for an
|
|
11
|
+
* approved host (via the egress proxy) — never handed to the child wholesale.
|
|
12
|
+
* The audit log only ever sees redacted values, never a restored secret.
|
|
13
|
+
*
|
|
14
|
+
* Pairs with credential-guard.js: that guard stops the AGENT reading secrets
|
|
15
|
+
* into model context; this stops the SUBPROCESS inheriting them. Both classify
|
|
16
|
+
* secret var names through the same `isSecretEnvName` so they never drift.
|
|
17
|
+
*
|
|
18
|
+
* Opt-in for now (`CC_CREDENTIAL_PROXY=1` or `config.credentialProxy.enabled`)
|
|
19
|
+
* so existing workflows that legitimately read a token from env keep working;
|
|
20
|
+
* default-on is the eventual goal once per-host injection is wired everywhere.
|
|
21
|
+
* Pure + dependency-light so every spawn seam can share it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { isSecretEnvName } from "./credential-guard.js";
|
|
25
|
+
|
|
26
|
+
/** Opaque replacement a masked credential var carries into the child env. */
|
|
27
|
+
export const CREDENTIAL_SENTINEL_PREFIX = "cc-cred-redacted:";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Well-known credential vars whose NAME does not obviously match the generic
|
|
31
|
+
* KEY/TOKEN/SECRET/PASSWORD pattern. Most real ones already match; this is a
|
|
32
|
+
* small safety net, extendable per-project via `deny`.
|
|
33
|
+
*/
|
|
34
|
+
const EXTRA_CREDENTIAL_NAMES = new Set([
|
|
35
|
+
"AWS_SESSION_TOKEN", // matches TOKEN, kept explicit
|
|
36
|
+
"GOOGLE_APPLICATION_CREDENTIALS", // path to a key file
|
|
37
|
+
"CLOUDSDK_AUTH_ACCESS_TOKEN",
|
|
38
|
+
"DIGITALOCEAN_ACCESS_TOKEN",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function toSet(value) {
|
|
42
|
+
if (value instanceof Set) return value;
|
|
43
|
+
return new Set(Array.isArray(value) ? value : value ? [value] : []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Classify an env var NAME as credential-bearing. `opts.allow` forces
|
|
48
|
+
* pass-through (never masked); `opts.deny` forces masking; otherwise the shared
|
|
49
|
+
* secret-name classifier + the curated extra set decide.
|
|
50
|
+
*/
|
|
51
|
+
export function isCredentialEnvName(name, opts = {}) {
|
|
52
|
+
if (typeof name !== "string" || !name) return false;
|
|
53
|
+
if (toSet(opts.allow).has(name)) return false;
|
|
54
|
+
if (toSet(opts.deny).has(name)) return true;
|
|
55
|
+
return isSecretEnvName(name) || EXTRA_CREDENTIAL_NAMES.has(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The sentinel a child sees in place of a credential's real value. */
|
|
59
|
+
export function makeSentinel(name) {
|
|
60
|
+
return CREDENTIAL_SENTINEL_PREFIX + String(name ?? "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True when a value is a proxy sentinel (carries no secret). */
|
|
64
|
+
export function isSentinel(value) {
|
|
65
|
+
return (
|
|
66
|
+
typeof value === "string" && value.startsWith(CREDENTIAL_SENTINEL_PREFIX)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mask credential-named vars in `env`. Returns a NEW object (never mutates the
|
|
72
|
+
* input):
|
|
73
|
+
* - `env` — credential vars replaced with a sentinel (mode "mask", the
|
|
74
|
+
* default) or removed entirely (mode "deny"); everything else
|
|
75
|
+
* copied through verbatim.
|
|
76
|
+
* - `masked` — sorted list of the var names that were masked/removed.
|
|
77
|
+
* - `vault` — Map<name, realValue> the PARENT keeps for approved injection;
|
|
78
|
+
* the child never receives it.
|
|
79
|
+
* Values that are already sentinels, null, or undefined are left as-is (a
|
|
80
|
+
* sentinel is never double-masked; an absent var is not invented).
|
|
81
|
+
*/
|
|
82
|
+
export function maskCredentialEnv(env = {}, opts = {}) {
|
|
83
|
+
const mode = opts.mode === "deny" ? "deny" : "mask";
|
|
84
|
+
const out = {};
|
|
85
|
+
const masked = [];
|
|
86
|
+
const vault = new Map();
|
|
87
|
+
for (const [name, value] of Object.entries(env || {})) {
|
|
88
|
+
if (
|
|
89
|
+
value != null &&
|
|
90
|
+
!isSentinel(value) &&
|
|
91
|
+
isCredentialEnvName(name, opts)
|
|
92
|
+
) {
|
|
93
|
+
masked.push(name);
|
|
94
|
+
vault.set(name, String(value));
|
|
95
|
+
if (mode === "mask") out[name] = makeSentinel(name);
|
|
96
|
+
// mode "deny": drop the var so the child does not even see a sentinel
|
|
97
|
+
} else {
|
|
98
|
+
out[name] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
masked.sort();
|
|
102
|
+
return { env: out, masked, vault };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Replace a secret value with a fixed marker — never the real value. */
|
|
106
|
+
export function redactSecretValue(value) {
|
|
107
|
+
if (value == null) return value;
|
|
108
|
+
return String(value) ? "***" : "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A log-safe projection of an env for the audit trail: credential values →
|
|
113
|
+
* "***", sentinels kept verbatim (they hold no secret), everything else
|
|
114
|
+
* untouched. NEVER emits a restored/real credential value.
|
|
115
|
+
*/
|
|
116
|
+
export function redactEnvForAudit(env = {}, opts = {}) {
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const [name, value] of Object.entries(env || {})) {
|
|
119
|
+
if (isSentinel(value)) out[name] = value;
|
|
120
|
+
else if (isCredentialEnvName(name, opts))
|
|
121
|
+
out[name] = redactSecretValue(value);
|
|
122
|
+
else out[name] = value;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the real value for a masked credential — but ONLY when the target
|
|
129
|
+
* host is on the approved list. The whole point of the proxy is that the child
|
|
130
|
+
* never gets the raw secret; the parent injects it just-in-time for an approved
|
|
131
|
+
* destination (e.g. the egress proxy adding Authorization for an allowed API
|
|
132
|
+
* host). Returns null (fail closed) for an unknown var, an empty host, or a
|
|
133
|
+
* non-approved host.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveApprovedInjection(vault, name, opts = {}) {
|
|
136
|
+
if (!(vault instanceof Map) || !vault.has(name)) return null;
|
|
137
|
+
const host = String(opts.host || "").toLowerCase();
|
|
138
|
+
if (!host) return null;
|
|
139
|
+
const approved = (opts.approvedHosts || []).map((h) =>
|
|
140
|
+
String(h || "").toLowerCase(),
|
|
141
|
+
);
|
|
142
|
+
return approved.includes(host) ? vault.get(name) : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve whether the credential proxy is enabled. Env var wins so a spawn
|
|
147
|
+
* seam without config access can still honor it; otherwise the config flag.
|
|
148
|
+
*/
|
|
149
|
+
export function credentialProxyEnabled(config = {}, env = process.env) {
|
|
150
|
+
const raw = env && env.CC_CREDENTIAL_PROXY;
|
|
151
|
+
if (raw != null) {
|
|
152
|
+
const v = String(raw).toLowerCase();
|
|
153
|
+
return v === "1" || v === "true" || v === "on" || v === "yes";
|
|
154
|
+
}
|
|
155
|
+
return config?.credentialProxy?.enabled === true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Apply the credential proxy to a child env when enabled; otherwise return the
|
|
160
|
+
* env UNCHANGED (same reference — the default path stays byte-identical). The
|
|
161
|
+
* returned `vault` lets the caller inject approved creds later.
|
|
162
|
+
*
|
|
163
|
+
* @returns {{env:object, masked:string[], vault:Map, enabled:boolean}}
|
|
164
|
+
*/
|
|
165
|
+
export function applyCredentialProxy(env, options = {}) {
|
|
166
|
+
const procEnv = options.env || process.env;
|
|
167
|
+
const config = options.config || {};
|
|
168
|
+
if (!credentialProxyEnabled(config, procEnv)) {
|
|
169
|
+
return { env, masked: [], vault: new Map(), enabled: false };
|
|
170
|
+
}
|
|
171
|
+
const allow = new Set([
|
|
172
|
+
...toSet(options.allow),
|
|
173
|
+
...(config?.credentialProxy?.allow || []),
|
|
174
|
+
]);
|
|
175
|
+
const deny = new Set([
|
|
176
|
+
...toSet(options.deny),
|
|
177
|
+
...(config?.credentialProxy?.deny || []),
|
|
178
|
+
]);
|
|
179
|
+
const mode = options.mode || config?.credentialProxy?.mode || "mask";
|
|
180
|
+
const result = maskCredentialEnv(env, { allow, deny, mode });
|
|
181
|
+
return { ...result, enabled: true };
|
|
182
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-stream gap-detection + replay primitives — the server→client (event)
|
|
3
|
+
* counterpart to the client→server RemoteCommandLedger (harness/
|
|
4
|
+
* remote-command-ledger.js). Both give a reconnecting peer an at-most-once,
|
|
5
|
+
* totally-ordered view over a lossy transport; the ledger orders *commands*,
|
|
6
|
+
* this orders *events*.
|
|
7
|
+
*
|
|
8
|
+
* A producer keeps a bounded buffer of recently emitted, seq-stamped frames
|
|
9
|
+
* (EventReplayBuffer); a reconnecting consumer reports its last-seen seq and
|
|
10
|
+
* gets the tail re-sent, or is told to full-resync when the missed range has
|
|
11
|
+
* already been evicted. A consumer detects holes with SeqGapTracker.
|
|
12
|
+
*
|
|
13
|
+
* Pure + deterministic: no I/O, no timers, no globals. The `seq` it stamps is
|
|
14
|
+
* the same additive protocol-v1 notion used on the stream-json surface
|
|
15
|
+
* (agent-sdk docs/PROTOCOL.md §1.2.1) — 1-based, monotonic, per producer
|
|
16
|
+
* instance; consumers MUST tolerate its absence and MUST NOT require gap-free
|
|
17
|
+
* numbering across a producer restart (a fresh instance rewinds to 1).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const DEFAULT_MAX_EVENTS = 512;
|
|
21
|
+
const DEFAULT_MAX_BYTES = 1 << 20; // 1 MiB — bound memory on a chatty session
|
|
22
|
+
|
|
23
|
+
/** Best-effort byte size of a frame (overridable for deterministic tests). */
|
|
24
|
+
function approxBytes(frame) {
|
|
25
|
+
try {
|
|
26
|
+
return Buffer.byteLength(JSON.stringify(frame));
|
|
27
|
+
} catch {
|
|
28
|
+
return 0; // circular / unserializable — don't let sizing throw
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Producer-side bounded replay buffer. `record(frame)` stamps the next `seq`
|
|
34
|
+
* onto a shallow copy, retains it (evicting the oldest beyond count/byte
|
|
35
|
+
* bounds), and returns the stamped frame ready to send. `replaySince(cursor)`
|
|
36
|
+
* returns the frames a consumer with last-seen seq `cursor` still needs.
|
|
37
|
+
*/
|
|
38
|
+
export class EventReplayBuffer {
|
|
39
|
+
constructor({
|
|
40
|
+
maxEvents = DEFAULT_MAX_EVENTS,
|
|
41
|
+
maxBytes = DEFAULT_MAX_BYTES,
|
|
42
|
+
sizeOf,
|
|
43
|
+
} = {}) {
|
|
44
|
+
this._max = Math.max(1, Math.floor(maxEvents) || DEFAULT_MAX_EVENTS);
|
|
45
|
+
this._maxBytes = Math.max(0, Math.floor(maxBytes) || 0);
|
|
46
|
+
this._sizeOf = typeof sizeOf === "function" ? sizeOf : approxBytes;
|
|
47
|
+
this._buf = []; // [{ seq, frame, bytes }] oldest → newest
|
|
48
|
+
this._seq = 0;
|
|
49
|
+
this._bytes = 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Highest seq stamped so far (0 before anything is recorded). */
|
|
53
|
+
get latestSeq() {
|
|
54
|
+
return this._seq;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Oldest seq still retained (== latestSeq when the buffer is empty). */
|
|
58
|
+
get oldestSeq() {
|
|
59
|
+
return this._buf.length ? this._buf[0].seq : this._seq;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Number of frames currently retained. */
|
|
63
|
+
get size() {
|
|
64
|
+
return this._buf.length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Stamp the next seq onto `frame` (shallow copy), retain it, evict overflow.
|
|
69
|
+
* @returns the stamped frame (`{ ...frame, seq }`) to hand straight to send.
|
|
70
|
+
*/
|
|
71
|
+
record(frame) {
|
|
72
|
+
const seq = ++this._seq;
|
|
73
|
+
const stamped = { ...frame, seq };
|
|
74
|
+
const bytes = this._sizeOf(stamped) || 0;
|
|
75
|
+
this._buf.push({ seq, frame: stamped, bytes });
|
|
76
|
+
this._bytes += bytes;
|
|
77
|
+
this._evict();
|
|
78
|
+
return stamped;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_evict() {
|
|
82
|
+
// Always keep at least the newest frame even if it alone exceeds maxBytes.
|
|
83
|
+
while (
|
|
84
|
+
this._buf.length > this._max ||
|
|
85
|
+
(this._maxBytes > 0 &&
|
|
86
|
+
this._bytes > this._maxBytes &&
|
|
87
|
+
this._buf.length > 1)
|
|
88
|
+
) {
|
|
89
|
+
const dropped = this._buf.shift();
|
|
90
|
+
this._bytes -= dropped.bytes;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Frames a consumer whose last-seen seq is `cursor` still needs.
|
|
96
|
+
* - `cursor >= latestSeq` → nothing missed → `{ frames: [], truncated: false }`.
|
|
97
|
+
* - `cursor` still within the retained window → the exact tail after it.
|
|
98
|
+
* - `cursor` older than what's retained → the gap was evicted →
|
|
99
|
+
* `truncated: true` (consumer MUST full-resync; the returned frames are
|
|
100
|
+
* only the retained suffix, not the whole hole).
|
|
101
|
+
* @param {number} cursor last seq the consumer acknowledged (default 0 = all)
|
|
102
|
+
*/
|
|
103
|
+
replaySince(cursor = 0) {
|
|
104
|
+
const c = Number.isFinite(cursor) ? cursor : 0;
|
|
105
|
+
if (c >= this._seq) {
|
|
106
|
+
return { frames: [], truncated: false, from: c, to: this._seq };
|
|
107
|
+
}
|
|
108
|
+
// The lowest cursor we can honor without a hole is (oldest retained − 1).
|
|
109
|
+
const floor = this._buf.length ? this._buf[0].seq - 1 : this._seq;
|
|
110
|
+
const truncated = c < floor;
|
|
111
|
+
const frames = this._buf.filter((e) => e.seq > c).map((e) => e.frame);
|
|
112
|
+
return { frames, truncated, from: c, to: this._seq };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const OK = Object.freeze({ status: "ok" });
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Consumer-side sequence tracker. Feed it each received frame's `seq`; it
|
|
120
|
+
* reports contiguity so the consumer can request a replay on a hole and drop
|
|
121
|
+
* duplicates a reconnect re-sent. One tracker per producer epoch — reset it
|
|
122
|
+
* when the transport hands you a fresh producer instance (e.g. a non-reattach
|
|
123
|
+
* bg-attach reply), since a fresh producer rewinds seq to 1.
|
|
124
|
+
*/
|
|
125
|
+
export class SeqGapTracker {
|
|
126
|
+
constructor() {
|
|
127
|
+
this._last = 0;
|
|
128
|
+
this._started = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Last contiguous seq accepted (0 before the first frame). */
|
|
132
|
+
get lastSeq() {
|
|
133
|
+
return this._last;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {number} seq the received frame's seq (absent/NaN on older
|
|
138
|
+
* producers that don't stamp — treated as always-ok, never a gap).
|
|
139
|
+
* @returns {{status:"ok"}|{status:"duplicate"}|{status:"gap",from:number,to:number}}
|
|
140
|
+
* - `ok`: contiguous (or the first frame, or an unstamped frame).
|
|
141
|
+
* - `duplicate`: `seq <= lastSeq` — already seen; drop it.
|
|
142
|
+
* - `gap`: `seq > lastSeq + 1` — frames `(from, to)` were missed; ask the
|
|
143
|
+
* producer for `replaySince(from)`. The tracker adopts `to` as current.
|
|
144
|
+
*/
|
|
145
|
+
observe(seq) {
|
|
146
|
+
if (!Number.isFinite(seq)) return OK; // unstamped producer — tolerate
|
|
147
|
+
if (!this._started) {
|
|
148
|
+
this._started = true;
|
|
149
|
+
this._last = seq;
|
|
150
|
+
return OK;
|
|
151
|
+
}
|
|
152
|
+
if (seq === this._last + 1) {
|
|
153
|
+
this._last = seq;
|
|
154
|
+
return OK;
|
|
155
|
+
}
|
|
156
|
+
if (seq <= this._last) return { status: "duplicate" };
|
|
157
|
+
const from = this._last;
|
|
158
|
+
this._last = seq; // adopt new position; caller replays the (from, to) hole
|
|
159
|
+
return { status: "gap", from, to: seq };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Forget all history (call when attaching to a fresh producer instance). */
|
|
163
|
+
reset() {
|
|
164
|
+
this._last = 0;
|
|
165
|
+
this._started = false;
|
|
166
|
+
}
|
|
167
|
+
}
|