chainlesschain 0.162.157 → 0.162.158
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/README.md +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CfMP2Oud.js → AIOps-BwpTJTN_.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Zu0N8BQL.js → ActionButton-BtQOoo3l.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DZTwj5T_.js → Analytics-Duf_rXZa.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CVFJh47v.js → AppLayout-XQK3wjvB.js} +5 -5
- package/src/assets/web-panel/assets/Artifacts-CTVt96zW.css +1 -0
- package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +1 -0
- package/src/assets/web-panel/assets/{Audit-BMfiLj7T.js → Audit-DNjOGQLh.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BslPCdGF.js → BackgroundAgents-FH9RAfB5.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CQ_fMC1r.js → Backup-ZIbrR4-U.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CsFsARiC.js → BaseInput-DSt3NaND.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CwfBobco.js → Chat-CY-wvbjv.js} +6 -6
- package/src/assets/web-panel/assets/Chat-KSGPtU0L.css +1 -0
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-K6Nf3xeN.js → ChatBubbleRenderer-BAV9V-t-.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DkSyjjPp.js → Checkbox-8JVadE_B.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B7Fx3zZ6.js → Codegen-CN83nPYI.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Dzfqn8Ep.js → Col-xPSvqdN8.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DhHPdzMM.js → Community-D78cnIjG.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Ct18_8y7.js → Compact-CSw6Csbp.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CY64IqBi.js → Compliance-E_HhjcYR.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYSmWlUl.js → Cowork-DybeQjmm.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-_4xtP3Ug.js → Cron-DS0BJeLY.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B4qY-Ycr.js → Crosschain-BOwVgkbs.js} +1 -1
- package/src/assets/web-panel/assets/{DID-CkCzWkx1.js → DID-DRUr9YuX.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-C1scT7Ke.js → Dashboard-Cht-yUKW.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DBnQ2EWd.js → Dropdown-BLr_EJ9X.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C9JhOTq0.js → EmailListRenderer-Drexuh5W.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWXv5RUv.js → FamilyGuardDashboard-D9Q2Kq63.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-fYdcbz9W.js → Federation-C-Z1xI-T.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CJqOT3Yj.js → FormItemContext-DLHPMkQF.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DGCsHc5K.js → GenericCardRenderer-DxxEdn2r.js} +1 -1
- package/src/assets/web-panel/assets/{Git-EH2K-H0Q.js → Git-syATcaBt.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DJn1X2pR.js → Governance-CYe_r8Zf.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Dch1E8qd.js → Inference-GWxErB7k.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-QIur8j8-.js → KnowledgeGraph-qQvVCZoi.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DUgHldPg.js → Logs-BenAU2W8.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-7agE3Or1.js +1 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-DbzhFwvz.css +1 -0
- package/src/assets/web-panel/assets/{Marketplace-DafrSfvd.js → Marketplace-D4Jn5fpD.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D2xRbfJx.js → McpTools-DtAwoqiQ.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-C6RbVHAo.js → Memory-BI_O50I8.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BPtVBKqn.js → MobileBridge-CQWE2lqx.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Cuw832cr.js → MobileProjects-CVtTqgtg.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-D6O9fWFa.js → Mtc-Dnr16qsn.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-CWZcXIH1.js → MtcAudit-CQ5Xe4-a.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-DhIBxTFd.js → Multisig-Bjr8O-wy.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-wkBFS33e.js → NLProgramming-CEe-KirU.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B09FxncA.js → Notes-spKJIKF9.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-oQwtH6-9.js → NotificationSettings-DRp83LFG.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-h1A0XB2a.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CsF-NIjz.js → Organization-BlgB0IYg.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-bLxv9Dqi.js → Overflow-a_35HqZI.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DVwQtm4q.js → P2P-CSNHCNPC.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Bgic3QKm.js → PdhVaultBrowser-C8M1JH-1.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-as80-9ep.js → Permissions-DmjJi9Ms.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-BGmUw36a.js → PersonalDataHub-DfuwRyf4.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-BM96zBxG.js → Pipeline-B2-S4ekE.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BBOzBSWe.js → Privacy-B1dOiBLC.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-GHX41V28.js → ProjectInit-_pNmc-DX.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DraqzxKD.js → ProjectSettings-DmwG2Ii1.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BzzdUml4.js → Projects-COATmHxF.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CSeYNAtW.js → Providers-C6HSjg1d.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DlzBCdXh.js → QrScannerModal-CrDmS7l1.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-p8iB7od_.js → QuickAsk-CfKvWt-x.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DjOVXacd.js → Recommend-DMa6Kuzv.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-DrktUo5x.js +4 -0
- package/src/assets/web-panel/assets/{Reputation-CjT3M0Di.js → Reputation-BTPQ1vjL.js} +1 -1
- package/src/assets/web-panel/assets/{Row-tw8m6T0c.js → Row-B6Q7QkMG.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B47KBx_f.js → RssFeed-B1IJhAmX.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D2sx1PBe.js → Search-xbea1kGD.js} +1 -1
- package/src/assets/web-panel/assets/{Security-JmtL8g7D.js → Security-7rs0cO3F.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BxrNqjin.js → Services-0fjixK0S.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-udk8L18t.js → Skeleton-B_rVq9Cu.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DooLT9of.js → Skills-C5uM90b9.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-ITVZE1xI.js → Sla-U4pYCIZH.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DCkfqY1n.js → SpeechSettings-Cdz4yTWx.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CkNlgFjM.js → SyncSettings-Me3aetHv.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BMAoyCM_.js → Tasks-Cf6aNkL_.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BnrFTlrz.js → Templates-CYatk4tU.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BjNsy3-i.js → Tenant-DIMKLw7o.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-C0gxLL7V.js → Terminal-C_cp-uSJ.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CUtj_KHA.js → TimelineRenderer-cUPVNADp.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DOg61zlq.js → Tokens-CiaEIXVf.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-D4ldCewj.js → Trigger-DB1WU5lQ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-3lk_Zd8j.js → Trust-Cfht9Ijy.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-kWhFR6Li.js → UkeySign-K3P4Vt9S.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-D3GOFVWg.js → VideoEditing-DWjhjXRh.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DzrZfBuh.js → Wallet-D9-Q1Kan.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CyVJVp_g.js → WebAuthn-BDszy-oV.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-i_b0wi9b.js → WorkflowEditor-DIhij4rA.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DwPCK8I7.js → chat-D7VCiKbh.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D0vaVqZU.js → colors-BvOd9pVn.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Don7nGW2.js → compact-item-DoqXBiAO.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D84R5qy2.js → createContext-CrKRzUtp.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BeUqVVKU.js → hasIn-BWAev4WC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BohrJpBf.js → index-1poMpMU0.js} +1 -1
- package/src/assets/web-panel/assets/{index-YKLCotoo.js → index-8g5TJCZ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8r7WKk-.js → index-B0vGEbJT.js} +1 -1
- package/src/assets/web-panel/assets/index-B1_pRTtB.js +1 -0
- package/src/assets/web-panel/assets/{index-m9bn0F1p.js → index-B30NvR21.js} +1 -1
- package/src/assets/web-panel/assets/{index-7QUwcGTg.js → index-BCyP071m.js} +1 -1
- package/src/assets/web-panel/assets/{index-4MkfPLGv.js → index-BHOcVYrx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZ1N07Sp.js → index-BN9m4Ux2.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFFzwrNs.js → index-BWWsaimP.js} +1 -1
- package/src/assets/web-panel/assets/{index-LMo9bBLJ.js → index-BXyl8O23.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlfTf0ka.js → index-BZGk8AbZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnnxzIx0.js → index-Bf9YlbDn.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAdCTTBC.js → index-Bj7EgmOp.js} +1 -1
- package/src/assets/web-panel/assets/{index-B57al0Z5.js → index-BkgnGX7v.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzT56x-_.js → index-BoYrS_L2.js} +1 -1
- package/src/assets/web-panel/assets/{index-L73NrmlW.js → index-Bq_ctRpz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CL71rIwo.js → index-C5kRmj3P.js} +1 -1
- package/src/assets/web-panel/assets/{index-WRw1pM5E.js → index-CDGLdn12.js} +1 -1
- package/src/assets/web-panel/assets/{index-hjgrIX5I.js → index-CEPHMSih.js} +1 -1
- package/src/assets/web-panel/assets/{index-IGGttsCv.js → index-CEXfKSiI.js} +1 -1
- package/src/assets/web-panel/assets/{index-D84BnuFg.js → index-CSb-tnIo.js} +1 -1
- package/src/assets/web-panel/assets/{index-x7VYlV41.js → index-CmJ6xKBO.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQor_7NF.js → index-CoD31Ndm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfYRv-zA.js → index-Cx7M7PSF.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBzBLX-K.js → index-D23L1V76.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMT8n9vB.js → index-DV-Rfmfx.js} +3 -3
- package/src/assets/web-panel/assets/{index-BER98o4P.js → index-DlGu7As6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrnVEEQO.js → index-DvDOpsJB.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4b5XqIy.js → index-HwHdTusY.js} +1 -1
- package/src/assets/web-panel/assets/{index-Br3SOdER.js → index-ISvPZ05r.js} +1 -1
- package/src/assets/web-panel/assets/{index-MKZrJrP_.js → index-JzUmJMvn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DT40zd7K.js → index-KR1-Em9C.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0_sKVi7.js → index-PDxGjKRu.js} +1 -1
- package/src/assets/web-panel/assets/index-TMM9FuW-.js +1 -0
- package/src/assets/web-panel/assets/{index-wesQQ2cH.js → index-YGDy7nNq.js} +1 -1
- package/src/assets/web-panel/assets/{index-DA0-EJKx.js → index-gmxqu5Kk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNpJf-sf.js → index-oY45XrkG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CExWtm98.js → index-ww1QtLnX.js} +1 -1
- package/src/assets/web-panel/assets/{index-De85D9Pc.js → index-zH-tuuGZ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DGeU4vPS.js → initDefaultProps-XW-OR3mL.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Dgqj9ab3.js → motion-CeuEHh20.js} +1 -1
- package/src/assets/web-panel/assets/{move-BFyEU6Lr.js → move-Dva79Keg.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CueCXXPC.js → mtc-parser-C0B6bD2j.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D2pU5OyE.js → omit-AkZvO-a2.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CKTf6zD6.js → pickAttrs-B4r55qGc.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DieJaGCN.js → placementArrow-BLH-z7YE.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Bxr-rmU6.js → responsiveObserve-ksEGZ2Wf.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CDEDk3Se.js → slide-Cth4OD1Z.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-icf2eCTx.js → statusUtils-9S83ezxw.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-qKvgmBhE.js → styleChecker-7yQmQGZ4.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-1_UhiyJZ.js → useFlexGapSupport-DpGnMmPc.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CFGLXDWh.js → useFs-C4zDZIXH.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-UafOXvQ6.js → usePersonalDataHub-CoLICdUZ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BfPqSx79.js → vnode-CKyH-35X.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CXsVcRB6.js → zoom-DRDGit17.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +4 -1
- package/src/data/changelog.json +7 -2
- package/src/lib/chrome-connector.js +39 -4
- package/src/lib/remote-control.js +35 -0
- package/src/lib/web-ui-server.js +120 -7
- package/src/repl/agent-repl.js +231 -58
- package/src/repl/remote-approval.js +111 -0
- package/src/runtime/agent-core.js +36 -0
- package/src/runtime/coding-agent-contract-shared.cjs +53 -0
- package/src/runtime/coding-agent-policy.cjs +16 -0
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/assets/web-panel/assets/Artifacts-BbN5V7au.js +0 -1
- package/src/assets/web-panel/assets/Artifacts-CRkepl4t.css +0 -1
- package/src/assets/web-panel/assets/Chat-DDh58PCf.css +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-iFKeNf6Y.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-CIGk7gk-.js +0 -4
- package/src/assets/web-panel/assets/devWarning-BBwWehQ7.js +0 -1
- package/src/assets/web-panel/assets/index-DJ8etkBT.js +0 -1
- package/src/assets/web-panel/assets/index-OeaK-zXZ.js +0 -1
package/src/repl/agent-repl.js
CHANGED
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
resolveAskIdleTimeoutMs,
|
|
112
112
|
questionWithIdleTimeout,
|
|
113
113
|
} from "./permission-prompt.js";
|
|
114
|
+
import { describeAskContext, raceLocalAndRemote } from "./remote-approval.js";
|
|
114
115
|
import {
|
|
115
116
|
parsePermissionTier,
|
|
116
117
|
parsePermissionModeArg,
|
|
@@ -448,6 +449,11 @@ export async function startAgentRepl(options = {}) {
|
|
|
448
449
|
// gate is wired. The wrapper is installed only when settings customize the
|
|
449
450
|
// map, and only bites while _sessionTier === "auto" (isActive predicate).
|
|
450
451
|
let _autoModeResolved = null;
|
|
452
|
+
// `/remote-control` (批17/18 REPL 收口): paired-device approvals for this
|
|
453
|
+
// interactive session. Holds { bridge, pairing, close } while active; both
|
|
454
|
+
// interactive confirmers below race the terminal against the paired device
|
|
455
|
+
// (first answer wins). null = local-only (byte-identical prompt path).
|
|
456
|
+
let _remoteApproval = null;
|
|
451
457
|
const baseUrl = options.baseUrl || "http://localhost:11434";
|
|
452
458
|
const apiKey = options.apiKey || null;
|
|
453
459
|
// Configured vision model (config.llm.visionModel) — used when a turn carries
|
|
@@ -617,54 +623,79 @@ export async function startAgentRepl(options = {}) {
|
|
|
617
623
|
_autoModeResolved = null; // fail to the plain gate tiers
|
|
618
624
|
}
|
|
619
625
|
if (typeof _approvalGate.setConfirmer === "function") {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
),
|
|
626
|
-
);
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
await _fireNotification(
|
|
630
|
-
`Permission needed: ${riskLevel || "medium"}-risk ${tool || "run_shell"}${args?.command ? " — " + args.command : ""}`,
|
|
631
|
-
);
|
|
626
|
+
// The local prompt as a cancelable handle so an active /remote-control
|
|
627
|
+
// can race it against a paired device: cancel closes the readline when
|
|
628
|
+
// the device answers first, and `canceled` mutes the stranded
|
|
629
|
+
// idle-timeout leg so it can't print "auto-denied" after the fact.
|
|
630
|
+
const askGateApprovalLocally = ({ tool, args, riskLevel }) => {
|
|
632
631
|
const rlConfirm = readline.createInterface({
|
|
633
632
|
input: process.stdin,
|
|
634
633
|
output: process.stdout,
|
|
635
634
|
});
|
|
635
|
+
let canceled = false;
|
|
636
636
|
const q = (p) => new Promise((res) => rlConfirm.question(p, res));
|
|
637
637
|
const cmd = args?.command ? ` ${args.command}` : "";
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
`\n[ApprovalGate] ${riskLevel || "medium"} risk command:${cmd}\n` +
|
|
642
|
-
` Proceed? [y]es once / [a]lways allow / [N]o: `,
|
|
643
|
-
),
|
|
644
|
-
_askIdleTimeoutMs,
|
|
645
|
-
);
|
|
646
|
-
rlConfirm.close();
|
|
647
|
-
if (res.timedOut) {
|
|
648
|
-
process.stdout.write(
|
|
638
|
+
const promise = (async () => {
|
|
639
|
+
const res = await questionWithIdleTimeout(
|
|
640
|
+
q,
|
|
649
641
|
chalk.yellow(
|
|
650
|
-
`\n
|
|
642
|
+
`\n[ApprovalGate] ${riskLevel || "medium"} risk command:${cmd}\n` +
|
|
643
|
+
` Proceed? [y]es once / [a]lways allow / [N]o: `,
|
|
651
644
|
),
|
|
645
|
+
_askIdleTimeoutMs,
|
|
652
646
|
);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (ans === "a" || ans === "always") {
|
|
657
|
-
const saved = await _persistAlwaysAllow(tool || "run_shell", args);
|
|
658
|
-
if (saved) {
|
|
647
|
+
rlConfirm.close();
|
|
648
|
+
if (canceled) return false; // raced out — the device already decided
|
|
649
|
+
if (res.timedOut) {
|
|
659
650
|
process.stdout.write(
|
|
660
|
-
chalk.
|
|
661
|
-
|
|
651
|
+
chalk.yellow(
|
|
652
|
+
`\n ⏱ no response in ${_askIdleTimeoutMs}ms — auto-denied\n`,
|
|
662
653
|
),
|
|
663
654
|
);
|
|
655
|
+
return false;
|
|
664
656
|
}
|
|
665
|
-
|
|
657
|
+
const ans = res.answer.trim().toLowerCase();
|
|
658
|
+
if (ans === "a" || ans === "always") {
|
|
659
|
+
const saved = await _persistAlwaysAllow(tool || "run_shell", args);
|
|
660
|
+
if (saved) {
|
|
661
|
+
process.stdout.write(
|
|
662
|
+
chalk.green(
|
|
663
|
+
` ✓ always allow: added ${saved.rule} → ${saved.file}\n`,
|
|
664
|
+
),
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
return ans === "y" || ans === "yes";
|
|
670
|
+
})();
|
|
671
|
+
return {
|
|
672
|
+
promise,
|
|
673
|
+
cancel: () => {
|
|
674
|
+
canceled = true;
|
|
675
|
+
rlConfirm.close();
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
};
|
|
679
|
+
_approvalGate.setConfirmer(async ({ tool, args, riskLevel }) => {
|
|
680
|
+
if (_dontAskActive()) {
|
|
681
|
+
process.stdout.write(
|
|
682
|
+
chalk.yellow(
|
|
683
|
+
`\n ✕ denied without asking (dontAsk mode): ${riskLevel || "medium"}-risk ${tool || "run_shell"}${args?.command ? " — " + args.command : ""}\n`,
|
|
684
|
+
),
|
|
685
|
+
);
|
|
686
|
+
return false;
|
|
666
687
|
}
|
|
667
|
-
|
|
688
|
+
await _fireNotification(
|
|
689
|
+
`Permission needed: ${riskLevel || "medium"}-risk ${tool || "run_shell"}${args?.command ? " — " + args.command : ""}`,
|
|
690
|
+
);
|
|
691
|
+
const local = askGateApprovalLocally({ tool, args, riskLevel });
|
|
692
|
+
if (!_remoteApproval?.bridge) return local.promise;
|
|
693
|
+
return raceLocalAndRemote({
|
|
694
|
+
bridge: _remoteApproval.bridge,
|
|
695
|
+
ask: describeAskContext({ tool, args, riskLevel }),
|
|
696
|
+
local,
|
|
697
|
+
writeOut: (text) => process.stdout.write(chalk.cyan(text)),
|
|
698
|
+
});
|
|
668
699
|
});
|
|
669
700
|
}
|
|
670
701
|
} catch (_err) {
|
|
@@ -693,45 +724,68 @@ export async function startAgentRepl(options = {}) {
|
|
|
693
724
|
loaded.rules.ask.length +
|
|
694
725
|
loaded.rules.deny.length;
|
|
695
726
|
_permissionRules = total > 0 ? loaded.rules : null;
|
|
696
|
-
//
|
|
697
|
-
// so
|
|
698
|
-
|
|
699
|
-
if (_dontAskActive()) {
|
|
700
|
-
process.stdout.write(
|
|
701
|
-
chalk.yellow(
|
|
702
|
-
`\n ✕ denied without asking (dontAsk mode): ${tool}${rule ? ` (${rule})` : reason ? ` — ${reason}` : ""}\n`,
|
|
703
|
-
),
|
|
704
|
-
);
|
|
705
|
-
return false;
|
|
706
|
-
}
|
|
707
|
-
await _fireNotification(
|
|
708
|
-
`Permission needed: ${tool}${rule ? " (" + rule + ")" : ""}`,
|
|
709
|
-
);
|
|
727
|
+
// Local prompt as a cancelable handle (same shape as the gate confirmer
|
|
728
|
+
// above) so /remote-control can race it against a paired device.
|
|
729
|
+
const askPermissionLocally = ({ tool, args, rule, reason }) => {
|
|
710
730
|
const rl = readline.createInterface({
|
|
711
731
|
input: process.stdin,
|
|
712
732
|
output: process.stdout,
|
|
713
733
|
});
|
|
734
|
+
let canceled = false;
|
|
714
735
|
const q = (p) => new Promise((res) => rl.question(p, res));
|
|
715
736
|
// Picks the right phrasing whether the caller passed a `rule`
|
|
716
737
|
// (settings/hook ask) or a `reason` (destructive-git / sensitive-file
|
|
717
738
|
// guards) — avoids the literal "null" the old template printed.
|
|
718
739
|
const header = buildPermissionPrompt({ tool, args, rule, reason });
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
740
|
+
const promise = (async () => {
|
|
741
|
+
const res = await questionWithIdleTimeout(
|
|
742
|
+
q,
|
|
743
|
+
chalk.yellow(`\n${header}\n Proceed? (y/N) `),
|
|
744
|
+
_askIdleTimeoutMs,
|
|
745
|
+
);
|
|
746
|
+
rl.close();
|
|
747
|
+
if (canceled) return false; // raced out — the device already decided
|
|
748
|
+
if (res.timedOut) {
|
|
749
|
+
process.stdout.write(
|
|
750
|
+
chalk.yellow(
|
|
751
|
+
`\n ⏱ no response in ${_askIdleTimeoutMs}ms — auto-denied\n`,
|
|
752
|
+
),
|
|
753
|
+
);
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
const ans = res.answer.trim().toLowerCase();
|
|
757
|
+
return ans === "y" || ans === "yes";
|
|
758
|
+
})();
|
|
759
|
+
return {
|
|
760
|
+
promise,
|
|
761
|
+
cancel: () => {
|
|
762
|
+
canceled = true;
|
|
763
|
+
rl.close();
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
};
|
|
767
|
+
// Confirmer is shared by permission `ask` rules AND hook `ask` decisions,
|
|
768
|
+
// so define it unconditionally (a `hook:` rule label flows through too).
|
|
769
|
+
_permissionConfirm = async ({ tool, args, rule, reason }) => {
|
|
770
|
+
if (_dontAskActive()) {
|
|
726
771
|
process.stdout.write(
|
|
727
772
|
chalk.yellow(
|
|
728
|
-
`\n
|
|
773
|
+
`\n ✕ denied without asking (dontAsk mode): ${tool}${rule ? ` (${rule})` : reason ? ` — ${reason}` : ""}\n`,
|
|
729
774
|
),
|
|
730
775
|
);
|
|
731
776
|
return false;
|
|
732
777
|
}
|
|
733
|
-
|
|
734
|
-
|
|
778
|
+
await _fireNotification(
|
|
779
|
+
`Permission needed: ${tool}${rule ? " (" + rule + ")" : ""}`,
|
|
780
|
+
);
|
|
781
|
+
const local = askPermissionLocally({ tool, args, rule, reason });
|
|
782
|
+
if (!_remoteApproval?.bridge) return local.promise;
|
|
783
|
+
return raceLocalAndRemote({
|
|
784
|
+
bridge: _remoteApproval.bridge,
|
|
785
|
+
ask: describeAskContext({ tool, args, rule, reason }),
|
|
786
|
+
local,
|
|
787
|
+
writeOut: (text) => process.stdout.write(chalk.cyan(text)),
|
|
788
|
+
});
|
|
735
789
|
};
|
|
736
790
|
} catch (_err) {
|
|
737
791
|
if (_err?.code === "CC_MANAGED_SETTINGS_INVALID") throw _err;
|
|
@@ -900,6 +954,60 @@ export async function startAgentRepl(options = {}) {
|
|
|
900
954
|
// Non-critical — SessionManager integration must not block startup
|
|
901
955
|
}
|
|
902
956
|
|
|
957
|
+
// `/remote-control` start/stop (批17 slash 入口 + 批18 REPL 接线): reuses
|
|
958
|
+
// the headless assembly (self-hosted WS on an OS-assigned port + a
|
|
959
|
+
// RemoteApprovalBridge for this session); the interactive difference is
|
|
960
|
+
// purely how the confirmers consume it — they RACE the terminal prompt
|
|
961
|
+
// against the paired device instead of gating on the remote alone.
|
|
962
|
+
const _startRemoteApproval = async () => {
|
|
963
|
+
const { startHeadlessRemoteApproval } =
|
|
964
|
+
await import("../lib/remote-approval-bridge.js");
|
|
965
|
+
const started = await startHeadlessRemoteApproval({
|
|
966
|
+
agentSessionId:
|
|
967
|
+
sessionId || `repl-${process.pid}-${Date.now().toString(36)}`,
|
|
968
|
+
isText: false, // the REPL prints its own pairing lines below
|
|
969
|
+
});
|
|
970
|
+
_remoteApproval = {
|
|
971
|
+
bridge: started.bridge,
|
|
972
|
+
pairing: started.pairing,
|
|
973
|
+
close: started.close,
|
|
974
|
+
};
|
|
975
|
+
logger.log(
|
|
976
|
+
chalk.cyan(
|
|
977
|
+
" remote-control: permission prompts can be answered from a paired device (first answer — terminal or device — wins)",
|
|
978
|
+
),
|
|
979
|
+
);
|
|
980
|
+
logger.log(` pairing: ${started.pairing.uri}`);
|
|
981
|
+
try {
|
|
982
|
+
const { renderQrCode } = await import("../lib/remote-control.js");
|
|
983
|
+
const qr = await renderQrCode(started.pairing.uri);
|
|
984
|
+
if (qr) logger.log(qr);
|
|
985
|
+
} catch (_err) {
|
|
986
|
+
// QR is progressive enhancement — the URI line is the contract
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
const _stopRemoteApproval = async () => {
|
|
990
|
+
const active = _remoteApproval;
|
|
991
|
+
_remoteApproval = null; // confirmers fall back to local-only immediately
|
|
992
|
+
if (active) await active.close().catch(() => undefined);
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// --remote-control also applies to interactive sessions. Headless fails
|
|
996
|
+
// closed when the bridge can't start (approvals would silently fall
|
|
997
|
+
// closed); interactively the terminal still prompts, so a start failure
|
|
998
|
+
// degrades to local-only with a warning instead of aborting the session.
|
|
999
|
+
if (options.remoteControl === true) {
|
|
1000
|
+
try {
|
|
1001
|
+
await _startRemoteApproval();
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
logger.log(
|
|
1004
|
+
chalk.yellow(
|
|
1005
|
+
` remote-control unavailable (${err.message}) — approvals stay local-only`,
|
|
1006
|
+
),
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
903
1011
|
// --system-prompt replaces the built-in prompt; --append-system-prompt
|
|
904
1012
|
// extends it (parity with the headless runners). The base is kept so an
|
|
905
1013
|
// output-style persona can be swapped in/out at runtime via /output-style.
|
|
@@ -1377,6 +1485,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
1377
1485
|
"/release-notes",
|
|
1378
1486
|
"/reload-plugins",
|
|
1379
1487
|
"/reload-skills",
|
|
1488
|
+
"/remote-control",
|
|
1380
1489
|
"/review",
|
|
1381
1490
|
"/rewind",
|
|
1382
1491
|
"/search",
|
|
@@ -1909,6 +2018,9 @@ export async function startAgentRepl(options = {}) {
|
|
|
1909
2018
|
logger.log(
|
|
1910
2019
|
` ${chalk.cyan("/permissions")} Allow/ask/deny rules; set/cycle tier (/permissions <tier> · Shift+Tab); /permissions denials to review blocked calls`,
|
|
1911
2020
|
);
|
|
2021
|
+
logger.log(
|
|
2022
|
+
` ${chalk.cyan("/remote-control")} Answer permission prompts from a paired phone/web device (on|off|status; alias /rc)`,
|
|
2023
|
+
);
|
|
1912
2024
|
logger.log(
|
|
1913
2025
|
` ${chalk.cyan("/export")} Save this conversation to a Markdown file (/export [path])`,
|
|
1914
2026
|
);
|
|
@@ -4028,6 +4140,57 @@ export async function startAgentRepl(options = {}) {
|
|
|
4028
4140
|
return;
|
|
4029
4141
|
}
|
|
4030
4142
|
|
|
4143
|
+
// `/remote-control` — pair a phone/web device to answer this session's
|
|
4144
|
+
// permission prompts (批17 slash 入口 + 批18 REPL 接线). First answer —
|
|
4145
|
+
// terminal or device — wins; devices get permission.request cards + push.
|
|
4146
|
+
if (
|
|
4147
|
+
trimmed === "/remote-control" ||
|
|
4148
|
+
trimmed.startsWith("/remote-control ") ||
|
|
4149
|
+
trimmed === "/rc" ||
|
|
4150
|
+
trimmed.startsWith("/rc ")
|
|
4151
|
+
) {
|
|
4152
|
+
const arg = trimmed
|
|
4153
|
+
.replace(/^\/(remote-control|rc)/, "")
|
|
4154
|
+
.trim()
|
|
4155
|
+
.toLowerCase();
|
|
4156
|
+
if (arg === "off" || arg === "stop") {
|
|
4157
|
+
if (!_remoteApproval) {
|
|
4158
|
+
logger.info("remote-control is not active.");
|
|
4159
|
+
} else {
|
|
4160
|
+
await _stopRemoteApproval();
|
|
4161
|
+
logger.info(
|
|
4162
|
+
"remote-control stopped — approvals are local-only again.",
|
|
4163
|
+
);
|
|
4164
|
+
}
|
|
4165
|
+
} else if (arg === "status") {
|
|
4166
|
+
if (!_remoteApproval) {
|
|
4167
|
+
logger.info("remote-control: off — start with /remote-control");
|
|
4168
|
+
} else {
|
|
4169
|
+
logger.log(` pairing: ${_remoteApproval.pairing.uri}`);
|
|
4170
|
+
const approvers = await _remoteApproval.bridge.approverCount();
|
|
4171
|
+
logger.log(
|
|
4172
|
+
` paired approvers: ${approvers}${approvers === 0 ? " (scan the pairing URI from the mobile app or web panel)" : ""}`,
|
|
4173
|
+
);
|
|
4174
|
+
}
|
|
4175
|
+
} else if (arg === "" || arg === "on" || arg === "start") {
|
|
4176
|
+
if (_remoteApproval) {
|
|
4177
|
+
logger.info(
|
|
4178
|
+
"remote-control already running — /remote-control status",
|
|
4179
|
+
);
|
|
4180
|
+
} else {
|
|
4181
|
+
try {
|
|
4182
|
+
await _startRemoteApproval();
|
|
4183
|
+
} catch (err) {
|
|
4184
|
+
logger.info(`remote-control failed to start: ${err.message}`);
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
} else {
|
|
4188
|
+
logger.info("Usage: /remote-control [on|off|status] (alias /rc)");
|
|
4189
|
+
}
|
|
4190
|
+
prompt();
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4031
4194
|
// `/cost` — running token spend + estimated $ for this session (Claude-Code
|
|
4032
4195
|
// parity). In-memory accumulation, so it works without session persistence.
|
|
4033
4196
|
if (trimmed === "/cost" || trimmed.startsWith("/cost ")) {
|
|
@@ -4968,6 +5131,16 @@ export async function startAgentRepl(options = {}) {
|
|
|
4968
5131
|
}
|
|
4969
5132
|
}
|
|
4970
5133
|
|
|
5134
|
+
// Tear down /remote-control (bridge + its self-hosted WS server) so the
|
|
5135
|
+
// pairing endpoint doesn't outlive the session.
|
|
5136
|
+
if (_remoteApproval) {
|
|
5137
|
+
try {
|
|
5138
|
+
await _stopRemoteApproval();
|
|
5139
|
+
} catch (_e) {
|
|
5140
|
+
// Non-critical
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
|
|
4971
5144
|
// Kill any background run_shell tasks so a backgrounded command (e.g. a
|
|
4972
5145
|
// dev server) doesn't outlive the REPL session.
|
|
4973
5146
|
try {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL-side remote-approval glue (gap-analysis 批26 — 批17/18 REPL 遗留收口).
|
|
3
|
+
*
|
|
4
|
+
* Interactive sessions race the LOCAL terminal prompt against a paired
|
|
5
|
+
* device's decision (headless gates on the remote decision alone):
|
|
6
|
+
*
|
|
7
|
+
* - local answer first → settle the device card too (resolveLocally
|
|
8
|
+
* publishes permission.resolved so the phone/web UI clears it)
|
|
9
|
+
* - remote answer first → cancel the local readline prompt and print
|
|
10
|
+
* the outcome
|
|
11
|
+
* - remote leg times out or the bridge closes mid-ask → NOT a decision:
|
|
12
|
+
* keep waiting on the terminal. A silent phone must never auto-deny a
|
|
13
|
+
* user sitting at the keyboard — the fail-closed timeout is headless
|
|
14
|
+
* semantics only.
|
|
15
|
+
*
|
|
16
|
+
* Pure over an injected bridge + local prompt handle ({promise, cancel}),
|
|
17
|
+
* so the race is unit-testable without a WS server or readline.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize the shapes the REPL's two confirmers receive (ApprovalGate
|
|
22
|
+
* `{tool, args, riskLevel}` / settings-hook `{tool, args, rule, reason}`)
|
|
23
|
+
* into the bridge's `{tool, action, detail}` ask payload.
|
|
24
|
+
*/
|
|
25
|
+
export function describeAskContext({
|
|
26
|
+
tool,
|
|
27
|
+
args,
|
|
28
|
+
rule = null,
|
|
29
|
+
reason = null,
|
|
30
|
+
riskLevel = null,
|
|
31
|
+
} = {}) {
|
|
32
|
+
const detail =
|
|
33
|
+
typeof args?.command === "string"
|
|
34
|
+
? args.command
|
|
35
|
+
: typeof args?.path === "string"
|
|
36
|
+
? args.path
|
|
37
|
+
: args
|
|
38
|
+
? JSON.stringify(args).slice(0, 2000)
|
|
39
|
+
: null;
|
|
40
|
+
return {
|
|
41
|
+
tool: tool || null,
|
|
42
|
+
action: rule
|
|
43
|
+
? `ask-rule:${rule}`
|
|
44
|
+
: reason || (riskLevel ? `${riskLevel}-risk` : null),
|
|
45
|
+
detail,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Remote outcomes that are NOT a decision for an interactive session: the
|
|
50
|
+
// devices went silent (timeout) or the bridge was torn down mid-ask (closed).
|
|
51
|
+
const NON_DECISIVE_REMOTE = new Set(["timeout", "closed"]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Race the local interactive prompt against the paired-device decision.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} opts
|
|
57
|
+
* @param {import("../lib/remote-approval-bridge.js").RemoteApprovalBridge} opts.bridge
|
|
58
|
+
* @param {{tool?, action?, detail?}} opts.ask - describeAskContext() payload
|
|
59
|
+
* @param {{promise: Promise<boolean>, cancel?: () => void}} opts.local -
|
|
60
|
+
* the already-started local prompt; `cancel` closes its readline when the
|
|
61
|
+
* device answers first (implementations must mute their idle-timeout
|
|
62
|
+
* message after cancel).
|
|
63
|
+
* @param {(text: string) => void} [opts.writeOut]
|
|
64
|
+
* @returns {Promise<boolean>} approved
|
|
65
|
+
*/
|
|
66
|
+
export async function raceLocalAndRemote({
|
|
67
|
+
bridge,
|
|
68
|
+
ask = {},
|
|
69
|
+
local,
|
|
70
|
+
writeOut = () => {},
|
|
71
|
+
}) {
|
|
72
|
+
let requestId = null;
|
|
73
|
+
const remoteLeg = bridge
|
|
74
|
+
.requestDecision({
|
|
75
|
+
...ask,
|
|
76
|
+
onRequestId: (id) => {
|
|
77
|
+
requestId = id;
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
.then((decision) => ({ src: "remote", decision }));
|
|
81
|
+
const localLeg = local.promise.then(
|
|
82
|
+
(approved) => ({ src: "local", approved: approved === true }),
|
|
83
|
+
() => ({ src: "local", approved: false }),
|
|
84
|
+
);
|
|
85
|
+
let winner = await Promise.race([remoteLeg, localLeg]);
|
|
86
|
+
if (
|
|
87
|
+
winner.src === "remote" &&
|
|
88
|
+
NON_DECISIVE_REMOTE.has(winner.decision?.via)
|
|
89
|
+
) {
|
|
90
|
+
winner = await localLeg;
|
|
91
|
+
}
|
|
92
|
+
if (winner.src === "local") {
|
|
93
|
+
if (requestId) {
|
|
94
|
+
try {
|
|
95
|
+
bridge.resolveLocally(requestId, winner.approved);
|
|
96
|
+
} catch {
|
|
97
|
+
// clearing the device card is best-effort
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return winner.approved;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
local.cancel?.();
|
|
104
|
+
} catch {
|
|
105
|
+
// readline already closed
|
|
106
|
+
}
|
|
107
|
+
writeOut(
|
|
108
|
+
`\n ✓ ${winner.decision.approved ? "allowed" : "denied"} from paired device\n`,
|
|
109
|
+
);
|
|
110
|
+
return winner.decision.approved === true;
|
|
111
|
+
}
|
|
@@ -3187,6 +3187,40 @@ async function executeToolInner(
|
|
|
3187
3187
|
}
|
|
3188
3188
|
}
|
|
3189
3189
|
|
|
3190
|
+
case "browser_state": {
|
|
3191
|
+
// P1 #8: first-class Chrome CDP observation — the agent side of the
|
|
3192
|
+
// "locate error → fix → verify" web loop against the user's real,
|
|
3193
|
+
// logged-in browser (`cc browse chrome launch`). DOM is capped harder
|
|
3194
|
+
// than the CLI command's 150k default so a page dump cannot flood the
|
|
3195
|
+
// conversation; screenshots go to a generated temp file (never an
|
|
3196
|
+
// agent-chosen path) to keep the tool strictly read-only.
|
|
3197
|
+
try {
|
|
3198
|
+
const { captureState } = await import("../lib/chrome-connector.js");
|
|
3199
|
+
const screenshotPath = args.screenshot
|
|
3200
|
+
? path.join(os.tmpdir(), `cc-browser-state-${Date.now()}.png`)
|
|
3201
|
+
: null;
|
|
3202
|
+
const state = await captureState({
|
|
3203
|
+
port: args.port != null ? Number(args.port) : undefined,
|
|
3204
|
+
tab: args.tab != null ? Number(args.tab) : undefined,
|
|
3205
|
+
reload: args.reload === true,
|
|
3206
|
+
watchMs: args.watch_ms != null ? Number(args.watch_ms) : undefined,
|
|
3207
|
+
includeDom: args.include_dom !== false,
|
|
3208
|
+
domCap: args.dom_cap != null ? Number(args.dom_cap) : 40000,
|
|
3209
|
+
screenshotPath,
|
|
3210
|
+
});
|
|
3211
|
+
if (!state.ok) {
|
|
3212
|
+
return attachDescriptor({
|
|
3213
|
+
error: `browser_state failed: ${state.error}`,
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
return attachDescriptor(state);
|
|
3217
|
+
} catch (err) {
|
|
3218
|
+
return attachDescriptor({
|
|
3219
|
+
error: `browser_state failed: ${err.message}`,
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3190
3224
|
case "search_files": {
|
|
3191
3225
|
// An explicit directory scopes the search to one root; otherwise span
|
|
3192
3226
|
// cwd plus any --add-dir roots so cross-package searches find matches.
|
|
@@ -6523,6 +6557,8 @@ export function formatToolArgs(name, args) {
|
|
|
6523
6557
|
return `${args.level || "info"}: ${(args.title || "").substring(0, 50)}`;
|
|
6524
6558
|
case "publish_artifact":
|
|
6525
6559
|
return `${args.kind || "other"}: ${(args.title || args.path || "").substring(0, 60)}`;
|
|
6560
|
+
case "browser_state":
|
|
6561
|
+
return `tab=${args.tab ?? 0} port=${args.port ?? 9222}${args.reload ? " reload" : ""}`;
|
|
6526
6562
|
case "schedule":
|
|
6527
6563
|
return args.action === "cron"
|
|
6528
6564
|
? `cron ${args.cron || ""}`.trim()
|
|
@@ -1005,6 +1005,59 @@ const CODING_AGENT_TOOL_CONTRACTS = Object.freeze([
|
|
|
1005
1005
|
],
|
|
1006
1006
|
},
|
|
1007
1007
|
},
|
|
1008
|
+
{
|
|
1009
|
+
name: "browser_state",
|
|
1010
|
+
title: "Browser State",
|
|
1011
|
+
kind: "browser",
|
|
1012
|
+
tier: "extension",
|
|
1013
|
+
description:
|
|
1014
|
+
"Capture live state from the user's real Chrome over CDP: current URL/title, open tabs, console messages and failed/error network requests observed during a short watch window, a capped DOM snapshot, and optionally a screenshot (saved to a temp file whose path is returned). Requires a debuggable Chrome — the user starts one with `cc browse chrome launch`. Console/network capture starts at attach time (CDP keeps no history) — set reload=true to observe load-time errors. Use to verify web changes in the user's logged-in browser.",
|
|
1015
|
+
inputSchema: {
|
|
1016
|
+
type: "object",
|
|
1017
|
+
properties: {
|
|
1018
|
+
port: {
|
|
1019
|
+
type: "number",
|
|
1020
|
+
description: "CDP port of the debuggable Chrome (default 9222)",
|
|
1021
|
+
},
|
|
1022
|
+
tab: {
|
|
1023
|
+
type: "number",
|
|
1024
|
+
description: "Tab index to observe (default 0)",
|
|
1025
|
+
},
|
|
1026
|
+
reload: {
|
|
1027
|
+
type: "boolean",
|
|
1028
|
+
description:
|
|
1029
|
+
"Reload the page first to capture load-time console/network (default false)",
|
|
1030
|
+
},
|
|
1031
|
+
watch_ms: {
|
|
1032
|
+
type: "number",
|
|
1033
|
+
description: "Observation window in milliseconds (default 3000)",
|
|
1034
|
+
},
|
|
1035
|
+
include_dom: {
|
|
1036
|
+
type: "boolean",
|
|
1037
|
+
description: "Include a capped HTML snapshot (default true)",
|
|
1038
|
+
},
|
|
1039
|
+
dom_cap: {
|
|
1040
|
+
type: "number",
|
|
1041
|
+
description: "Max HTML characters to return (default 40000)",
|
|
1042
|
+
},
|
|
1043
|
+
screenshot: {
|
|
1044
|
+
type: "boolean",
|
|
1045
|
+
description:
|
|
1046
|
+
"Save a screenshot to a generated temp file and return its path (default false)",
|
|
1047
|
+
},
|
|
1048
|
+
},
|
|
1049
|
+
required: [],
|
|
1050
|
+
},
|
|
1051
|
+
...TOOL_POLICY_METADATA.browser_state,
|
|
1052
|
+
permissions: {
|
|
1053
|
+
level: "standard",
|
|
1054
|
+
scopes: ["browser:read"],
|
|
1055
|
+
},
|
|
1056
|
+
telemetry: {
|
|
1057
|
+
category: "browser",
|
|
1058
|
+
tags: ["tool:browser_state", "contract:coding-agent", "tier:extension"],
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1008
1061
|
]);
|
|
1009
1062
|
|
|
1010
1063
|
const CODING_AGENT_MVP_TOOL_NAMES = Object.freeze(
|
|
@@ -304,6 +304,22 @@ const TOOL_POLICY_METADATA = Object.freeze({
|
|
|
304
304
|
approvalFlow: "auto",
|
|
305
305
|
isReadOnly: false,
|
|
306
306
|
},
|
|
307
|
+
// Attaches to the user's own debuggable Chrome over loopback CDP and
|
|
308
|
+
// observes one tab (console/network/DOM/optional screenshot) for a bounded
|
|
309
|
+
// window. Observation only — it never clicks, types, or navigates; the
|
|
310
|
+
// optional reload re-runs the page the user already has open, and the
|
|
311
|
+
// screenshot goes to a generated temp path (never an agent-chosen one), so
|
|
312
|
+
// the tool stays read-only and plan-mode safe.
|
|
313
|
+
browser_state: {
|
|
314
|
+
riskLevel: RISK_LEVELS.LOW,
|
|
315
|
+
category: TOOL_CATEGORIES.READ,
|
|
316
|
+
availableInPlanMode: true,
|
|
317
|
+
planModeBehavior: "allow",
|
|
318
|
+
requiresPlanApproval: false,
|
|
319
|
+
requiresConfirmation: false,
|
|
320
|
+
approvalFlow: "auto",
|
|
321
|
+
isReadOnly: true,
|
|
322
|
+
},
|
|
307
323
|
});
|
|
308
324
|
|
|
309
325
|
function normalizeGitCommand(command) {
|
|
@@ -56,6 +56,9 @@ export function resolveAgentPolicy({
|
|
|
56
56
|
pdh: overrides.pdh,
|
|
57
57
|
outputStyle: overrides.outputStyle,
|
|
58
58
|
disableSlashCommands: overrides.disableSlashCommands === true,
|
|
59
|
+
// --remote-control also applies interactively: the REPL starts the
|
|
60
|
+
// paired-device approval bridge at startup (批26).
|
|
61
|
+
remoteControl: overrides.remoteControl === true,
|
|
59
62
|
};
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{E as V,b as z,r as b,I as w,J as y,U as m,c as i,K as r,S as A,V as S,o as F,f as N,P as d,Q as p,R as v,F as O}from"./vendor-Bn1JzzAn.js";import{u as h,_ as M,d as I}from"./index-DMT8n9vB.js";import{R as U}from"./icons-DG5UD-6K.js";const E=V("artifacts",()=>{const x=b([]),n=b(!1),u=b(""),e=b(""),f=b(null),_=b(!1),C=z(()=>[...new Set(x.value.map(a=>a.kind).filter(Boolean))].sort());async function s(){const t=h();n.value=!0;try{const a={type:"artifact-list"};u.value&&(a.kind=u.value),e.value&&(a.session=e.value);const o=await t.sendRaw(a);o&&Array.isArray(o.artifacts)&&(x.value=[...o.artifacts].reverse())}catch{}finally{n.value=!1}}async function l(t){const a=h();_.value=!0,f.value={entry:t,previewable:!1};try{const o=await a.sendRaw({type:"artifact-content",artifactId:t.id});f.value={entry:t,previewable:o?.previewable===!0,encoding:o?.encoding||null,content:o?.content||"",truncated:o?.truncated===!0,reason:o?.reason||null}}catch(o){f.value={entry:t,previewable:!1,reason:o?.message||"preview failed"}}finally{_.value=!1}}function k(){f.value=null}const g=z(()=>{const t=f.value;return!t?.previewable||t.encoding!=="base64"?null:`data:${t.entry?.mime||"image/png"};base64,${t.content}`});async function R(t){const a=h();if(!t)return!1;try{const o=await a.sendRaw({type:"artifact-remove",artifactId:t});return f.value?.entry?.id===t&&(f.value=null),await s(),o?.found===!0}catch{return!1}}async function B(){const t=h();try{const a=await t.sendRaw({type:"artifact-clean"});return await s(),Number(a?.removed)||0}catch{return 0}}function P(t){const a=Number(t)||0;return a<1024?`${a} B`:a<1024*1024?`${(a/1024).toFixed(1)} KB`:`${(a/(1024*1024)).toFixed(1)} MB`}function c(t){switch(t){case"report":return"blue";case"patch":return"purple";case"screenshot":return"cyan";case"log":return"default";case"data":return"green";default:return"default"}}return{artifacts:x,loading:n,kindFilter:u,sessionFilter:e,kinds:C,preview:f,previewLoading:_,previewImageSrc:g,fetchArtifacts:s,openPreview:l,closePreview:k,removeArtifact:R,cleanExpired:B,formatSize:P,kindColor:c}}),J={__name:"Artifacts",setup(x,{expose:n}){n();const u=E(),e=[{title:"标题",key:"title",dataIndex:"title",ellipsis:!0},{title:"类型",key:"kind",dataIndex:"kind",width:110},{title:"MIME",dataIndex:"mime",width:160,ellipsis:!0},{title:"大小",key:"size",dataIndex:"size",width:90},{title:"发布时间",key:"createdAt",dataIndex:"createdAt",width:170},{title:"会话",dataIndex:"sessionId",width:160,ellipsis:!0},{title:"操作",key:"actions",width:150}],f=z(()=>u.kinds.map(l=>({label:l,value:l})));async function _(l){await u.removeArtifact(l)?I.success("已删除"):I.error("删除失败")}async function C(){const l=await u.cleanExpired();I.info(l>0?`清理了 ${l} 个过期交付物`:"没有过期交付物")}F(()=>{u.fetchArtifacts()}),N(()=>{u.closePreview()});const s={store:u,columns:e,kindOptions:f,onRemove:_,onClean:C,computed:z,onBeforeUnmount:N,onMounted:F,get ReloadOutlined(){return U},get message(){return I},get useArtifactsStore(){return E}};return Object.defineProperty(s,"__isScriptSetup",{enumerable:!1,value:!0}),s}},K={class:"page-header"},L=["onClick"],T=["src"],j={key:1,class:"preview-text"},D={class:"preview-meta"},Q={key:0};function W(x,n,u,e,f,_){const C=w("a-select"),s=w("a-button"),l=w("a-popconfirm"),k=w("a-space"),g=w("a-tag"),R=w("a-table"),B=w("a-empty"),P=w("a-card");return d(),y("div",null,[m("div",K,[n[6]||(n[6]=m("div",null,[m("h2",{class:"page-title"},"交付物"),m("p",{class:"page-sub"}," agent 用 publish_artifact 发布的交付物(报告 / patch / 截图 / 日志),在这里预览与管理(cc artifacts 同源)。 ")],-1)),i(k,null,{default:r(()=>[i(C,{value:e.store.kindFilter,"onUpdate:value":n[0]||(n[0]=c=>e.store.kindFilter=c),style:{"min-width":"120px"},placeholder:"全部类型","allow-clear":"",options:e.kindOptions,onChange:n[1]||(n[1]=c=>e.store.fetchArtifacts())},null,8,["value","options"]),i(l,{title:"删除所有过期交付物?",onConfirm:e.onClean},{default:r(()=>[i(s,null,{default:r(()=>[...n[4]||(n[4]=[p("清理过期",-1)])]),_:1})]),_:1}),i(s,{ghost:"",loading:e.store.loading,onClick:n[2]||(n[2]=c=>e.store.fetchArtifacts())},{icon:r(()=>[i(e.ReloadOutlined)]),default:r(()=>[n[5]||(n[5]=p(" 刷新 ",-1))]),_:1},8,["loading"])]),_:1})]),i(R,{"data-source":e.store.artifacts,columns:e.columns,"row-key":"id",size:"small",pagination:{pageSize:10,hideOnSinglePage:!0}},{bodyCell:r(({column:c,record:t})=>[c.key==="title"?(d(),y("a",{key:0,onClick:a=>e.store.openPreview(t)},v(t.title),9,L)):c.key==="kind"?(d(),A(g,{key:1,color:e.store.kindColor(t.kind)},{default:r(()=>[p(v(t.kind),1)]),_:2},1032,["color"])):c.key==="size"?(d(),y(O,{key:2},[p(v(e.store.formatSize(t.size)),1)],64)):c.key==="createdAt"?(d(),y(O,{key:3},[p(v((t.createdAt||"").replace("T"," ").slice(0,19)),1)],64)):c.key==="actions"?(d(),A(k,{key:4},{default:r(()=>[i(s,{size:"small",onClick:a=>e.store.openPreview(t)},{default:r(()=>[...n[7]||(n[7]=[p("预览",-1)])]),_:1},8,["onClick"]),i(l,{title:"删除该交付物?",onConfirm:a=>e.onRemove(t.id)},{default:r(()=>[i(s,{size:"small",danger:""},{default:r(()=>[...n[8]||(n[8]=[p("删除",-1)])]),_:1})]),_:1},8,["onConfirm"])]),_:2},1024)):S("v-if",!0)]),_:1},8,["data-source"]),e.store.preview?(d(),A(P,{key:0,size:"small",class:"preview-card",loading:e.store.previewLoading},{title:r(()=>[i(k,null,{default:r(()=>[m("span",null,v(e.store.preview.entry?.title),1),i(g,{color:e.store.kindColor(e.store.preview.entry?.kind)},{default:r(()=>[p(v(e.store.preview.entry?.kind),1)]),_:1},8,["color"]),i(g,{color:"default"},{default:r(()=>[p(v(e.store.preview.entry?.mime),1)]),_:1}),e.store.preview.truncated?(d(),A(g,{key:0,color:"warning"},{default:r(()=>[...n[9]||(n[9]=[p("已截断预览",-1)])]),_:1})):S("v-if",!0)]),_:1})]),extra:r(()=>[i(s,{size:"small",onClick:n[3]||(n[3]=c=>e.store.closePreview())},{default:r(()=>[...n[10]||(n[10]=[p("关闭",-1)])]),_:1})]),default:r(()=>[e.store.previewImageSrc?(d(),y("img",{key:0,src:e.store.previewImageSrc,class:"preview-image",alt:"artifact preview"},null,8,T)):e.store.preview.previewable?(d(),y("pre",j,v(e.store.preview.content||"(空文件)"),1)):(d(),A(B,{key:2,description:e.store.preview.reason||"该类型不支持在线预览,可在本机用 cc artifacts open <id> 打开"},null,8,["description"])),m("div",D,[m("span",null,v(e.store.preview.entry?.id),1),e.store.preview.entry?.sessionId?(d(),y("span",Q," · session "+v(e.store.preview.entry.sessionId),1)):S("v-if",!0),m("span",null," · sha256 "+v((e.store.preview.entry?.sha256||"").slice(0,12))+"…",1)])]),_:1},8,["loading"])):S("v-if",!0)])}const X=M(J,[["render",W],["__scopeId","data-v-c8b48f8c"],["__file","/tmp/cc-web-panel-i5N2Ja/repo/packages/web-panel/src/views/Artifacts.vue"]]);export{X as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.page-header[data-v-c8b48f8c]{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px}.preview-card[data-v-c8b48f8c]{margin-top:16px}.preview-text[data-v-c8b48f8c]{max-height:480px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:12px;margin:0}.preview-image[data-v-c8b48f8c]{max-width:100%;max-height:480px;display:block}.preview-meta[data-v-c8b48f8c]{margin-top:8px;color:#808080cc;font-size:12px}
|