chainlesschain 0.162.125 → 0.162.127
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 +2 -2
- package/src/assets/web-panel/assets/{AIOps-DcqNhZhA.js → AIOps-CsHJh6tT.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BigtmPoq.js → ActionButton-ClDr78Wl.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-1J_X_HF4.js → Analytics-CQQBX5mv.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DlcDG_a_.js → AppLayout-DIofBNeG.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-Do-y8_3w.js → Audit-d8pxGJLK.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-S2Df-50Y.js → Backup-ChZb31ek.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-D5kgWJfk.js → BaseInput-CQsSXb5v.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-aUcp3kN1.js → Chat-C_mjILx8.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-B545kWla.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-DTg1U7Xs.js → Checkbox-CWFmVuo8.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-YO9ZZ4Ky.js → Codegen-DwicePOQ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BHonE9fU.js → Col-B36SnRdL.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DA2AlEFh.js → Community-C_sZ2HhK.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DULxLRNg.js → Compact-B2jzDcUl.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BZqfuuZL.js → Compliance-BD58_IHe.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CJe3vyMS.js → Cowork-VIBdGc4D.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CeV8AtRu.js → Cron-CDTVWUmV.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CAg66zS5.js → Crosschain-D5DkGNKU.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Dtup5JZm.js → DID-B23ae8PQ.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DAR_8PNy.js → Dashboard-DTG5MsSx.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-K5bTaCYN.js → Dropdown-D35RRnMZ.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C890uErx.js → EmailListRenderer-DRvY2nPk.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-B-Tj_8yM.js → FamilyGuardDashboard-BT_eESkk.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C6WZ98ni.js → Federation-CxX9F7IL.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-D-LTfGWd.js → FormItemContext-C1xDz6D8.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-C80DK4W7.js → GenericCardRenderer-CjBOAXcO.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Cjvvv3zW.js → Git-I9I9NkPO.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-C0BND77H.js → Governance-BiigWxQW.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BL9kTtSu.js → Inference-C0KrSCgh.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CnVTBmJf.js → KnowledgeGraph-B1Nd54ow.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CbCbg7K6.js → Logs-CeOtvYIo.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CG5On-fH.js → Marketplace-CRhgvAVQ.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-AYYDZZK7.js → McpTools-BIJNah-H.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-tMWbMDQq.js → Memory-D6gsbo4e.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BAOsf4wB.js → MobileBridge-DDkIKA6U.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BT-2komQ.js → MobileProjects-BBkI02Lf.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BFwfC63n.js → Mtc-BziHK7HS.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-DYG3CWdH.js → MtcAudit-CWXjbkeF.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-KJwd0yWT.js → Multisig-9jNF_VWa.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BmL5dNWm.js → NLProgramming-BUdF7I2G.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DmkaVaMc.js → Notes-D5ls1r1T.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-tmVQszDZ.js → NotificationSettings-B2WX_TMX.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BtrR7anf.js → OrderTableRenderer-COSzdTcE.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-pppktQyW.js → Organization-tzWIgnbM.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-D3lBoQDA.js → Overflow-Ca1gp_3S.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BgE2qGlZ.js → P2P-XnfDfRK9.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-D26Bz5gS.js → PdhVaultBrowser-23Susb25.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BR8no2Xe.js → Permissions-CnkhYPqt.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DabbyQEF.js → PersonalDataHub-CZ1UasJc.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-n4sNpEz8.js → Pipeline-DC1oC84K.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CGNp4n8M.js → Privacy-CdqayaCE.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CAVA5VsX.js → ProjectInit-DS7NbGQL.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-GCgkfM7A.js → ProjectSettings-fdzwup3n.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BYK17p52.js → Projects-FvodeZob.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DpUT5W-d.js → Providers-IxYyzeSI.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-ZJxTb7vi.js → QuickAsk-BAtFET-B.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CIzFRDk1.js → Recommend-DCNRV9Vr.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-lQ_WDNZn.js → Reputation-lg2JmIai.js} +1 -1
- package/src/assets/web-panel/assets/{Row-1MEtrgtF.js → Row-BAhyDuri.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-SSgcPPl4.js → RssFeed-BA_3issD.js} +2 -2
- package/src/assets/web-panel/assets/{Search-CcTMOGNY.js → Search-CJheUJDl.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BA1KMaDx.js → Security-D2VczQDD.js} +3 -3
- package/src/assets/web-panel/assets/{Services-_aj7czZn.js → Services-7ZFAzqVR.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BWgg_0Zr.js → Skeleton-CsBoTasD.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C2ZIziYM.js → Skills-DT_j_gj0.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CYxp4axY.js → Sla-C9D5geJx.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C7Csp1jV.js → SpeechSettings-CWCNVW42.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-svGeswiw.js → SyncSettings-TYCbf2kk.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-ClISj6oK.js → Tasks-CbRoXu1G.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CmO-Fp7r.js → Templates-CxBjIrkJ.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-a3Ac3lxz.js → Tenant-NKVyFsPM.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DyJIRh81.js → Terminal-D5aAL5_H.js} +2 -2
- package/src/assets/web-panel/assets/TimelineRenderer-CD1JE0Rb.js +1 -0
- package/src/assets/web-panel/assets/{Tokens-DJW0KqV4.js → Tokens-CdepBSYe.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-0HeTi4r6.js → Trigger-Br3n_PEh.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-aFym34eo.js → Trust-CSac0pMf.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CYhszHJv.js → UkeySign-BiWo43zc.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BbJxPF9X.js → VideoEditing-Dj50GuoF.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DDthEwiu.js → Wallet-CQK_g4_U.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DQ6McYPg.js → WebAuthn-D8mBl1NC.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-CnF8zRsi.js → WorkflowEditor-CHeS_vab.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Dzkx2gTP.js → chat-CbeOf_lH.js} +1 -1
- package/src/assets/web-panel/assets/{colors-35T51Oo5.js → colors-CTFiyfed.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-TtzNrlu1.js → compact-item-CZsh8FXE.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Y1LkWrYb.js → createContext-UPhnXsap.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BFRqfgsk.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BOYw1BgS.js → hasIn-CCqQtSY-.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2od7Bgx.js → index-1d4pat4L.js} +1 -1
- package/src/assets/web-panel/assets/{index-NM9Oke2k.js → index-86dmMEZY.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dof7lWA_.js → index-B-OcSTNA.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7dCBw_M.js → index-B1tylGKq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqhwG1ox.js → index-BAcem_Qs.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZ16GlOs.js → index-BKADY9Iw.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPGNc2tF.js → index-Be0uH7m1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAizH273.js → index-Bga0UuWA.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFhlelzN.js → index-Blgzobgm.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6tG4B25.js → index-BwPULPuj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjeUZxE5.js → index-CIQVlE9u.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLqC2sRT.js → index-CMi91z74.js} +1 -1
- package/src/assets/web-panel/assets/{index-hyqEKkBT.js → index-CNteMctn.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cs82BHAj.js → index-CQHQiYAO.js} +1 -1
- package/src/assets/web-panel/assets/{index-zF77jnI2.js → index-CVKFSFkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-G4ClSmJc.js → index-CctoAd9I.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhMSOd_2.js → index-CdiuRSqw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWgNn9As.js → index-Cy83zglX.js} +3 -3
- package/src/assets/web-panel/assets/{index-Bw1iOGhM.js → index-DBJjqjxL.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4BgCBa3.js → index-DFbyobqi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKRlWHUp.js → index-DcJxrM1F.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7aKAZzS.js → index-DgXn2sWa.js} +1 -1
- package/src/assets/web-panel/assets/{index-D47robkX.js → index-Dkn1r09O.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVrUs6rf.js → index-DmScDPXc.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bs69SI96.js → index-DpTaXoo4.js} +1 -1
- package/src/assets/web-panel/assets/index-DqADH38Q.js +1 -0
- package/src/assets/web-panel/assets/{index-BxWUEFKy.js → index-DvaDSd9k.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtlJWaSP.js → index-LIFKNtNq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGPEaeB1.js → index-LyzbfmcN.js} +1 -1
- package/src/assets/web-panel/assets/index-Pbx8iJt_.js +1 -0
- package/src/assets/web-panel/assets/{index-KgXrlfNF.js → index-WGmri9tu.js} +1 -1
- package/src/assets/web-panel/assets/{index-XIuZV9by.js → index-Yd1x-xhs.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dhfw-BXs.js → index-_OOipSkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dx0hIfC-.js → index-dwBroN51.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7Wt5feN.js → index-e6tj_vqt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdyFg4FI.js → index-lOxvi-gf.js} +1 -1
- package/src/assets/web-panel/assets/{index-8jffdb6b.js → index-qTrskpW-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmkG479D.js → index-uH-glcG6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSpFe6j5.js → index-z5pyzCXj.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DD5y5msp.js → initDefaultProps-K95PVa57.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BuoutMia.js → motion-DwTSyvvW.js} +1 -1
- package/src/assets/web-panel/assets/{move-DkpjqOXg.js → move-BuRSogkN.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BmGJD-v-.js → mtc-parser-twEnPl1V.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DftUE0Sz.js → omit-CdPhebkX.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DDRb9FIu.js → pickAttrs-CGcIpdPi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DPgtxOOD.js → placementArrow-DixkX9VV.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BQFRzFeZ.js → responsiveObserve-C74t3rB5.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DQ4lmUSz.js → slide-TcNt6K6l.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-C1TR4ZiQ.js → statusUtils-mpTN9mrS.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B-Suj978.js → styleChecker-Bwg8zmIj.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Q7bDZ3EI.js → useFlexGapSupport-Cz4mN4sW.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DHjRLyQH.js → useFs-DF9zRfbI.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-lQOvCvCr.js → usePersonalDataHub-BcQ4O2UK.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Ie0g4-p3.js → vnode-juM2dgAV.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CHP0YEoH.js → zoom-c4aJj-vC.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +41 -2
- package/src/commands/compliance.js +3 -1
- package/src/commands/review.js +47 -17
- package/src/commands/session.js +11 -2
- package/src/gateways/ws/message-dispatcher.js +145 -165
- package/src/gateways/ws/ws-session-gateway.js +39 -5
- package/src/harness/jsonl-session-store.js +39 -12
- package/src/harness/worktree-isolator.js +5 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/audit-logger.js +3 -1
- package/src/lib/chat-core.js +5 -2
- package/src/lib/chat-intent-service.js +68 -21
- package/src/lib/config-manager.js +25 -1
- package/src/lib/credential-guard.js +43 -1
- package/src/lib/git-integration.js +5 -0
- package/src/lib/interaction-adapter.js +32 -11
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/llm-config-defaults.js +37 -0
- package/src/lib/mcp-oauth.js +76 -10
- package/src/lib/permission-engine.js +4 -1
- package/src/lib/personal-data-hub-wiring.js +14 -0
- package/src/lib/pipeline-orchestrator.js +20 -2
- package/src/lib/project-detector.js +44 -3
- package/src/lib/project-instructions.js +13 -0
- package/src/lib/repl-denials.js +137 -0
- package/src/lib/safe-json.js +22 -0
- package/src/lib/sandbox-v2.js +7 -6
- package/src/lib/search-command.js +65 -0
- package/src/lib/settings-hooks.cjs +133 -0
- package/src/lib/settings-loader.cjs +16 -7
- package/src/lib/social-graph.js +3 -1
- package/src/lib/stream-retry.js +27 -0
- package/src/lib/turn-context.js +15 -5
- package/src/repl/agent-repl.js +81 -8
- package/src/runtime/__tests__/message-roles.test.js +36 -3
- package/src/runtime/agent-core.js +175 -50
- package/src/runtime/coding-agent-contract-shared.cjs +9 -2
- package/src/runtime/coding-agent-shell-policy.cjs +23 -2
- package/src/runtime/file-ref-expander.js +51 -7
- package/src/runtime/headless-runner.js +83 -2
- package/src/runtime/headless-stream.js +69 -3
- package/src/runtime/mcp-config.js +42 -1
- package/src/runtime/message-roles.js +14 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Pg7dIsiE.js +0 -1
- package/src/assets/web-panel/assets/TimelineRenderer-acXS5N5h.js +0 -1
- package/src/assets/web-panel/assets/devWarning-D_lwLZ6O.js +0 -1
- package/src/assets/web-panel/assets/index-CEwuCM44.js +0 -1
- package/src/assets/web-panel/assets/index-DzzgU4IU.js +0 -1
package/src/repl/agent-repl.js
CHANGED
|
@@ -29,6 +29,11 @@ import {
|
|
|
29
29
|
analyzeContinuation,
|
|
30
30
|
joinContinuation,
|
|
31
31
|
} from "../lib/repl-multiline.js";
|
|
32
|
+
import {
|
|
33
|
+
classifyDenial,
|
|
34
|
+
recordDenial,
|
|
35
|
+
formatDenials,
|
|
36
|
+
} from "../lib/repl-denials.js";
|
|
32
37
|
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
33
38
|
import {
|
|
34
39
|
createSession,
|
|
@@ -118,6 +123,14 @@ let _settingsHooks = null;
|
|
|
118
123
|
// `!command` auto-triggers an assistant response to its output. undefined =
|
|
119
124
|
// unset → defaults OFF (opt-in) in shouldRespondToBashCommands.
|
|
120
125
|
let _respondToBash;
|
|
126
|
+
// .claude/settings.json `autoMode.classifyAllShell` (Claude-Code 2.1.193): route
|
|
127
|
+
// the built-in verification allowlist through the shell-policy classifier (→
|
|
128
|
+
// ApprovalGate confirm) instead of fast-pathing it. false = unset → off.
|
|
129
|
+
let _classifyAllShell = false;
|
|
130
|
+
// Bounded log of tool calls the agent was BLOCKED from running this session
|
|
131
|
+
// (shell-policy / ApprovalGate / settings rule / hook). Surfaced by
|
|
132
|
+
// `/permissions denials` (Claude-Code 2.1.193 "recent denials"). In-memory only.
|
|
133
|
+
const _recentDenials = [];
|
|
121
134
|
|
|
122
135
|
/**
|
|
123
136
|
* Fire settings.json Notification hooks (observe-only) — the agent needs the
|
|
@@ -180,13 +193,17 @@ async function executeTool(name, args) {
|
|
|
180
193
|
permissionRules: _permissionRules,
|
|
181
194
|
permissionConfirm: _permissionConfirm,
|
|
182
195
|
settingsHooks: _settingsHooks,
|
|
196
|
+
classifyAllShell: _classifyAllShell,
|
|
183
197
|
});
|
|
184
198
|
}
|
|
185
199
|
|
|
186
200
|
/**
|
|
187
201
|
* Agentic loop — wraps agent-core's async generator with REPL display output.
|
|
202
|
+
* Exported so its event-translation contract (checkpoint-mark accuracy for
|
|
203
|
+
* `/rewind`, content/usage extraction, forced-off in-loop compaction) is
|
|
204
|
+
* unit-testable via the `options._coreLoop` injection seam.
|
|
188
205
|
*/
|
|
189
|
-
async function agentLoop(messages, options) {
|
|
206
|
+
export async function agentLoop(messages, options) {
|
|
190
207
|
// Resume-degenerate role merge (Claude Code 2.1.187 parity), gated by the
|
|
191
208
|
// one-shot `mergeRoles` flag so it fires only on the first model call after
|
|
192
209
|
// resuming a session whose prior run produced no assistant response. Collapse
|
|
@@ -212,12 +229,19 @@ async function agentLoop(messages, options) {
|
|
|
212
229
|
`\n ⚠️ ${info.message || `已从 "${info.from}" 切换到 "${info.to}"`}\n`,
|
|
213
230
|
),
|
|
214
231
|
));
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
232
|
+
// `_coreLoop` is an injectable seam (defaults to agent-core's loop) so the
|
|
233
|
+
// wrapper's event translation can be unit-tested without a live model.
|
|
234
|
+
const runCoreLoop = options._coreLoop || coreAgentLoop;
|
|
235
|
+
// The tool-result event carries no args; remember the last tool-executing
|
|
236
|
+
// pair so a recorded denial can show what was attempted (/permissions denials).
|
|
237
|
+
let _lastExec = null;
|
|
238
|
+
for await (const event of runCoreLoop(messages, {
|
|
220
239
|
...options,
|
|
240
|
+
// FORCE in-loop compaction off — the REPL runs its OWN post-turn compaction
|
|
241
|
+
// (with metrics + persisted compact events), so letting agent-core compact
|
|
242
|
+
// too would trim the same history twice. Placed AFTER the spread so a
|
|
243
|
+
// caller's options.autoCompact can never silently re-enable the double pass.
|
|
244
|
+
autoCompact: false,
|
|
221
245
|
onProviderFallback,
|
|
222
246
|
})) {
|
|
223
247
|
if (event.type === "checkpoint") {
|
|
@@ -236,6 +260,7 @@ async function agentLoop(messages, options) {
|
|
|
236
260
|
chalk.gray(` ⎌ checkpoint ${event.id} (before ${event.tool})\n`),
|
|
237
261
|
);
|
|
238
262
|
} else if (event.type === "tool-executing") {
|
|
263
|
+
_lastExec = { tool: event.tool, args: event.args };
|
|
239
264
|
process.stdout.write(
|
|
240
265
|
chalk.gray(
|
|
241
266
|
` [${event.tool}] ${formatToolArgs(event.tool, event.args)}\n`,
|
|
@@ -246,6 +271,24 @@ async function agentLoop(messages, options) {
|
|
|
246
271
|
process.stdout.write(
|
|
247
272
|
chalk.red(` Error: ${event.error || event.result?.error}\n`),
|
|
248
273
|
);
|
|
274
|
+
// Record policy denials (not plain tool failures) into the caller's
|
|
275
|
+
// denial log for review via `/permissions denials` (Claude-Code 2.1.193
|
|
276
|
+
// recent denials). Caller passes the session log (mirrors checkpointMarks)
|
|
277
|
+
// so the wrapper stays unit-testable.
|
|
278
|
+
if (Array.isArray(options.denialLog)) {
|
|
279
|
+
const denial = classifyDenial({
|
|
280
|
+
tool: event.tool,
|
|
281
|
+
result: event.result,
|
|
282
|
+
error: event.error,
|
|
283
|
+
argsSummary:
|
|
284
|
+
_lastExec && _lastExec.tool === event.tool
|
|
285
|
+
? formatToolArgs(event.tool, _lastExec.args)
|
|
286
|
+
: "",
|
|
287
|
+
});
|
|
288
|
+
if (denial) {
|
|
289
|
+
recordDenial(options.denialLog, { ...denial, at: Date.now() });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
249
292
|
// Parity with Desktop AIChatPage's `Switch to Trusted` button:
|
|
250
293
|
// when the deny came from ApprovalGate (not shell-policy), surface
|
|
251
294
|
// the exact CLI command the user can run to relax the per-session
|
|
@@ -503,6 +546,11 @@ export async function startAgentRepl(options = {}) {
|
|
|
503
546
|
_respondToBash = readBooleanSetting("respondToBashCommands", {
|
|
504
547
|
cwd: process.cwd(),
|
|
505
548
|
});
|
|
549
|
+
// Claude-Code 2.1.193 autoMode.classifyAllShell (default OFF when unset).
|
|
550
|
+
_classifyAllShell =
|
|
551
|
+
readBooleanSetting("autoMode.classifyAllShell", {
|
|
552
|
+
cwd: process.cwd(),
|
|
553
|
+
}) === true;
|
|
506
554
|
const total =
|
|
507
555
|
loaded.rules.allow.length +
|
|
508
556
|
loaded.rules.ask.length +
|
|
@@ -538,12 +586,21 @@ export async function startAgentRepl(options = {}) {
|
|
|
538
586
|
// PostToolUse). The interactive _permissionConfirm above doubles as the
|
|
539
587
|
// confirmer for a hook `ask` decision.
|
|
540
588
|
try {
|
|
541
|
-
const { loadHooks } =
|
|
589
|
+
const { loadHooks, projectHookTrustNotice } =
|
|
590
|
+
await import("../lib/settings-hooks.cjs");
|
|
542
591
|
const loaded = loadHooks({ cwd: process.cwd() });
|
|
543
592
|
_settingsHooks =
|
|
544
593
|
loaded.hooks && Object.keys(loaded.hooks).length > 0
|
|
545
594
|
? loaded.hooks
|
|
546
595
|
: null;
|
|
596
|
+
// First-run trust notice for an untrusted/cloned repo's shell-running
|
|
597
|
+
// hooks (Claude-Code 2.1.195 parity). Best-effort, stderr-only.
|
|
598
|
+
try {
|
|
599
|
+
const notice = projectHookTrustNotice({ cwd: process.cwd() });
|
|
600
|
+
if (notice) process.stderr.write(notice + "\n");
|
|
601
|
+
} catch {
|
|
602
|
+
/* trust notice is best-effort */
|
|
603
|
+
}
|
|
547
604
|
} catch (_err) {
|
|
548
605
|
_settingsHooks = null;
|
|
549
606
|
}
|
|
@@ -1482,7 +1539,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
1482
1539
|
` ${chalk.cyan("/cost")} Session token spend + estimated $ (per model & category)`,
|
|
1483
1540
|
);
|
|
1484
1541
|
logger.log(
|
|
1485
|
-
` ${chalk.cyan("/permissions")} Allow/ask/deny rules; set/cycle tier (/permissions <tier> · Shift+Tab
|
|
1542
|
+
` ${chalk.cyan("/permissions")} Allow/ask/deny rules; set/cycle tier (/permissions <tier> · Shift+Tab); /permissions denials to review blocked calls`,
|
|
1486
1543
|
);
|
|
1487
1544
|
logger.log(
|
|
1488
1545
|
` ${chalk.cyan("/export")} Save this conversation to a Markdown file (/export [path])`,
|
|
@@ -3034,6 +3091,13 @@ export async function startAgentRepl(options = {}) {
|
|
|
3034
3091
|
// parity): what the agent runs unprompted, asks about, or is blocked from.
|
|
3035
3092
|
if (trimmed === "/permissions" || trimmed.startsWith("/permissions ")) {
|
|
3036
3093
|
const arg = trimmed.slice("/permissions".length).trim();
|
|
3094
|
+
if (arg === "denials" || arg === "denied") {
|
|
3095
|
+
// Review what the agent was BLOCKED from running this session
|
|
3096
|
+
// (Claude-Code 2.1.193 "recent denials").
|
|
3097
|
+
logger.log(formatDenials(_recentDenials));
|
|
3098
|
+
prompt();
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3037
3101
|
if (arg) {
|
|
3038
3102
|
// Set this session's approval tier mid-session (Claude-Code
|
|
3039
3103
|
// permission-mode / Shift+Tab parity; mirrors `cc session policy --set`).
|
|
@@ -3079,6 +3143,13 @@ export async function startAgentRepl(options = {}) {
|
|
|
3079
3143
|
" Set tier mid-session: /permissions <strict|trusted|autopilot>",
|
|
3080
3144
|
),
|
|
3081
3145
|
);
|
|
3146
|
+
if (_recentDenials.length) {
|
|
3147
|
+
logger.log(
|
|
3148
|
+
chalk.gray(
|
|
3149
|
+
` ${_recentDenials.length} tool call(s) denied this session — /permissions denials to review`,
|
|
3150
|
+
),
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3082
3153
|
prompt();
|
|
3083
3154
|
return;
|
|
3084
3155
|
}
|
|
@@ -3511,11 +3582,13 @@ export async function startAgentRepl(options = {}) {
|
|
|
3511
3582
|
autoCheckpoint,
|
|
3512
3583
|
checkpointSession: sessionId,
|
|
3513
3584
|
checkpointMarks: _checkpointMarks,
|
|
3585
|
+
denialLog: _recentDenials,
|
|
3514
3586
|
prepareCall,
|
|
3515
3587
|
approvalGate: _approvalGate,
|
|
3516
3588
|
permissionRules: _permissionRules,
|
|
3517
3589
|
permissionConfirm: _permissionConfirm,
|
|
3518
3590
|
settingsHooks: _settingsHooks,
|
|
3591
|
+
classifyAllShell: _classifyAllShell,
|
|
3519
3592
|
// Interactive session: gate run_code through the ApprovalGate (like
|
|
3520
3593
|
// run_shell) so a strict tier prompts before arbitrary code runs.
|
|
3521
3594
|
interactiveApproval: true,
|
|
@@ -51,13 +51,38 @@ describe("mergeConsecutiveMessages", () => {
|
|
|
51
51
|
// A multi-tool turn produces back-to-back `tool` results — folding them
|
|
52
52
|
// would drop tool_call_ids and corrupt the tool-call pairing.
|
|
53
53
|
const msgs = [
|
|
54
|
-
{
|
|
54
|
+
{
|
|
55
|
+
role: "assistant",
|
|
56
|
+
content: "",
|
|
57
|
+
tool_calls: [{ id: "a" }, { id: "b" }],
|
|
58
|
+
},
|
|
55
59
|
{ role: "tool", content: "r1", tool_call_id: "a" },
|
|
56
60
|
{ role: "tool", content: "r2", tool_call_id: "b" },
|
|
57
61
|
];
|
|
58
62
|
expect(mergeConsecutiveMessages(msgs)).toEqual(msgs);
|
|
59
63
|
});
|
|
60
64
|
|
|
65
|
+
it("never merges a structured assistant tool-call turn (tool_calls preserved)", () => {
|
|
66
|
+
// Two consecutive assistants where the SECOND carries tool_calls — folding
|
|
67
|
+
// would drop them (orphaning the result) and the strict API would 400.
|
|
68
|
+
const msgs = [
|
|
69
|
+
{ role: "assistant", content: "let me check" },
|
|
70
|
+
{ role: "assistant", content: "", tool_calls: [{ id: "c1" }] },
|
|
71
|
+
{ role: "tool", content: "result", tool_call_id: "c1" },
|
|
72
|
+
];
|
|
73
|
+
expect(mergeConsecutiveMessages(msgs)).toEqual(msgs);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("never folds two consecutive assistants when the FIRST has tool_calls", () => {
|
|
77
|
+
// e.g. after a compaction dropped the tool results — merging would mutate
|
|
78
|
+
// the structured turn's content and keep its now-unanswered tool_calls.
|
|
79
|
+
const msgs = [
|
|
80
|
+
{ role: "assistant", content: "", tool_calls: [{ id: "a" }] },
|
|
81
|
+
{ role: "assistant", content: "fallback text" },
|
|
82
|
+
];
|
|
83
|
+
expect(mergeConsecutiveMessages(msgs)).toEqual(msgs);
|
|
84
|
+
});
|
|
85
|
+
|
|
61
86
|
it("never merges system messages (leading base + context pair kept distinct)", () => {
|
|
62
87
|
const msgs = [
|
|
63
88
|
{ role: "system", content: "base" },
|
|
@@ -202,14 +227,22 @@ describe("collapseConsecutiveMessagesInPlace", () => {
|
|
|
202
227
|
const messages = [
|
|
203
228
|
{ role: "user", content: "u1" },
|
|
204
229
|
{ role: "user", content: "u2" },
|
|
205
|
-
{
|
|
230
|
+
{
|
|
231
|
+
role: "assistant",
|
|
232
|
+
content: "",
|
|
233
|
+
tool_calls: [{ id: "a" }, { id: "b" }],
|
|
234
|
+
},
|
|
206
235
|
{ role: "tool", content: "r1", tool_call_id: "a" },
|
|
207
236
|
{ role: "tool", content: "r2", tool_call_id: "b" },
|
|
208
237
|
];
|
|
209
238
|
expect(collapseConsecutiveMessagesInPlace(messages)).toBe(true);
|
|
210
239
|
expect(messages).toEqual([
|
|
211
240
|
{ role: "user", content: "u1\n\nu2" },
|
|
212
|
-
{
|
|
241
|
+
{
|
|
242
|
+
role: "assistant",
|
|
243
|
+
content: "",
|
|
244
|
+
tool_calls: [{ id: "a" }, { id: "b" }],
|
|
245
|
+
},
|
|
213
246
|
{ role: "tool", content: "r1", tool_call_id: "a" },
|
|
214
247
|
{ role: "tool", content: "r2", tool_call_id: "b" },
|
|
215
248
|
]);
|
|
@@ -43,10 +43,11 @@ import {
|
|
|
43
43
|
import { createToolContext } from "../tools/tool-context.js";
|
|
44
44
|
import { createToolTelemetryRecord } from "../tools/tool-telemetry.js";
|
|
45
45
|
import { isAbortError, throwIfAborted } from "../lib/abort-utils.js";
|
|
46
|
+
import { buildSearchCommand } from "../lib/search-command.js";
|
|
46
47
|
import {
|
|
47
48
|
isRetryableStreamError,
|
|
48
|
-
STREAM_RETRY_MAX,
|
|
49
49
|
STREAM_RETRY_BASE_MS,
|
|
50
|
+
resolveStreamRetryMax,
|
|
50
51
|
} from "../lib/stream-retry.js";
|
|
51
52
|
import {
|
|
52
53
|
annotateLines,
|
|
@@ -755,6 +756,27 @@ export function tokenizeShellWords(input) {
|
|
|
755
756
|
return args;
|
|
756
757
|
}
|
|
757
758
|
|
|
759
|
+
/**
|
|
760
|
+
* Literal string edit shared by the edit_file preview + execution paths.
|
|
761
|
+
* Uses split/join so `old_string` and `new_string` are treated as LITERAL text
|
|
762
|
+
* (no regex / `$&` / `$1` pattern interpretation — a plain `String.replace`
|
|
763
|
+
* with a string pattern still expands `$` sequences in the replacement).
|
|
764
|
+
* Returns the occurrence count so the caller can require a UNIQUE match
|
|
765
|
+
* (Claude-Code Edit parity): replacing the first of several identical strings
|
|
766
|
+
* silently edits the wrong place.
|
|
767
|
+
*
|
|
768
|
+
* @returns {{ count:number, newContent:string }}
|
|
769
|
+
*/
|
|
770
|
+
function applyLiteralEdit(content, oldStr, newStr, replaceAll) {
|
|
771
|
+
const parts = content.split(oldStr);
|
|
772
|
+
const count = parts.length - 1;
|
|
773
|
+
if (count === 0) return { count: 0, newContent: content };
|
|
774
|
+
const newContent = replaceAll
|
|
775
|
+
? parts.join(newStr)
|
|
776
|
+
: parts[0] + newStr + parts.slice(1).join(oldStr); // first occurrence only
|
|
777
|
+
return { count, newContent };
|
|
778
|
+
}
|
|
779
|
+
|
|
758
780
|
/**
|
|
759
781
|
* Compute the content an edit tool WOULD write, without writing it — the
|
|
760
782
|
* left/right sides for an IDE diff review. Mirrors the corresponding
|
|
@@ -781,15 +803,22 @@ export function computeProposedEdit(name, args = {}, cwd = process.cwd()) {
|
|
|
781
803
|
const content = fs.readFileSync(filePath, "utf8");
|
|
782
804
|
if (
|
|
783
805
|
typeof args.old_string !== "string" ||
|
|
784
|
-
|
|
806
|
+
args.old_string === "" ||
|
|
807
|
+
typeof args.new_string !== "string"
|
|
785
808
|
) {
|
|
786
809
|
return null;
|
|
787
810
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
811
|
+
const replaceAll = args.replace_all === true;
|
|
812
|
+
const { count, newContent } = applyLiteralEdit(
|
|
813
|
+
content,
|
|
814
|
+
args.old_string,
|
|
815
|
+
args.new_string,
|
|
816
|
+
replaceAll,
|
|
817
|
+
);
|
|
818
|
+
// No match, or a non-unique match without replace_all → the edit will be
|
|
819
|
+
// rejected, so show no (misleading) diff and let the tool report it.
|
|
820
|
+
if (count === 0 || (count > 1 && !replaceAll)) return null;
|
|
821
|
+
return { filePath, newContent, originalText: content };
|
|
793
822
|
}
|
|
794
823
|
if (name === "edit_file_hashed") {
|
|
795
824
|
if (!fs.existsSync(filePath)) return null;
|
|
@@ -965,7 +994,7 @@ export async function executeTool(name, args, context = {}) {
|
|
|
965
994
|
// 1. settings deny
|
|
966
995
|
if (settingsVerdict.decision === "deny") {
|
|
967
996
|
return {
|
|
968
|
-
error: `[Permission] Tool "${name}" denied by settings rule: ${settingsVerdict.rule}
|
|
997
|
+
error: `[Permission] Tool "${name}" denied by settings rule: ${settingsVerdict.rule}. This is a configured policy — retrying won't help; tell the user if the task genuinely needs it.`,
|
|
969
998
|
policy: { decision: "deny", rule: settingsVerdict.rule, via: "settings" },
|
|
970
999
|
};
|
|
971
1000
|
}
|
|
@@ -1037,7 +1066,7 @@ export async function executeTool(name, args, context = {}) {
|
|
|
1037
1066
|
: false;
|
|
1038
1067
|
if (!ok) {
|
|
1039
1068
|
return {
|
|
1040
|
-
error: `[Permission] Tool "${name}" requires confirmation (settings rule: ${settingsVerdict.rule}) — denied.`,
|
|
1069
|
+
error: `[Permission] Tool "${name}" requires confirmation (settings rule: ${settingsVerdict.rule}) but this run is non-interactive — denied. Do not retry; tell the user this action needs their approval.`,
|
|
1041
1070
|
policy: {
|
|
1042
1071
|
decision: "ask",
|
|
1043
1072
|
rule: settingsVerdict.rule,
|
|
@@ -1594,6 +1623,47 @@ export function renderNotebook(text) {
|
|
|
1594
1623
|
return lines.join("\n");
|
|
1595
1624
|
}
|
|
1596
1625
|
|
|
1626
|
+
/**
|
|
1627
|
+
* Ingest the raw stdout of a search_files command into the shared hit
|
|
1628
|
+
* accumulator: split into lines, dedup via `seen`, redact credential-file
|
|
1629
|
+
* content hits, label multi-root results, and stop at the hit cap. Pure aside
|
|
1630
|
+
* from mutating the passed-in `seen`/`hits`/`redactedCreds` collectors.
|
|
1631
|
+
*
|
|
1632
|
+
* Shared by the success path AND the maxBuffer-overflow salvage path: a search
|
|
1633
|
+
* on a large tree (especially Windows, where `findstr`/`dir /s` have no `head`
|
|
1634
|
+
* cap) can exceed execSync's maxBuffer and throw ENOBUFS — but the error still
|
|
1635
|
+
* carries the first maxBuffer of matches in `err.stdout`, so the same ingest is
|
|
1636
|
+
* run on it instead of dropping every match as a false "no matches".
|
|
1637
|
+
*
|
|
1638
|
+
* @param {string} output raw command stdout (or a truncated partial)
|
|
1639
|
+
* @param {object} ctx { isContent, root, multiRoot, seen, hits, redactedCreds,
|
|
1640
|
+
* credentialFileReason, limit? }
|
|
1641
|
+
*/
|
|
1642
|
+
export function _ingestSearchOutput(output, ctx) {
|
|
1643
|
+
const limit = ctx.limit ?? 20;
|
|
1644
|
+
for (const line of String(output || "")
|
|
1645
|
+
.trim()
|
|
1646
|
+
.split("\n")) {
|
|
1647
|
+
const v = line.trim();
|
|
1648
|
+
if (!v || ctx.seen.has(v)) continue;
|
|
1649
|
+
if (ctx.isContent) {
|
|
1650
|
+
// content hit is `file:line:text` (findstr /n) or a bare filename
|
|
1651
|
+
// (grep -l); pull the source file and redact credential matches.
|
|
1652
|
+
const src = v.match(/^(.+?):\d+:/)?.[1] ?? v;
|
|
1653
|
+
if (ctx.credentialFileReason(src)) {
|
|
1654
|
+
ctx.redactedCreds.add(src.replace(/\\/g, "/"));
|
|
1655
|
+
ctx.seen.add(v);
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
// Qualify with the root so multi-root results stay unambiguous.
|
|
1660
|
+
const labeled = ctx.multiRoot ? `${ctx.root}: ${v}` : v;
|
|
1661
|
+
ctx.seen.add(v);
|
|
1662
|
+
ctx.hits.push(labeled);
|
|
1663
|
+
if (ctx.hits.length >= limit) return;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1597
1667
|
/**
|
|
1598
1668
|
* Inner tool execution — no hooks, no plan-mode checks.
|
|
1599
1669
|
*/
|
|
@@ -1612,6 +1682,7 @@ async function executeToolInner(
|
|
|
1612
1682
|
mcpClient,
|
|
1613
1683
|
llmOptions,
|
|
1614
1684
|
shellPolicyOverrides,
|
|
1685
|
+
classifyAllShell = false,
|
|
1615
1686
|
approvalGate,
|
|
1616
1687
|
shellConfirm,
|
|
1617
1688
|
additionalDirectories,
|
|
@@ -1663,6 +1734,13 @@ async function executeToolInner(
|
|
|
1663
1734
|
if (!fs.existsSync(filePath)) {
|
|
1664
1735
|
return attachDescriptor({ error: `File not found: ${filePath}` });
|
|
1665
1736
|
}
|
|
1737
|
+
// A clear, self-correcting error beats the cryptic "EISDIR: illegal
|
|
1738
|
+
// operation on a directory" that readFileSync throws on a directory.
|
|
1739
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
1740
|
+
return attachDescriptor({
|
|
1741
|
+
error: `Path is a directory, not a file: ${filePath}. Use list_dir to see its contents.`,
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1666
1744
|
const content = fs.readFileSync(filePath, "utf8");
|
|
1667
1745
|
// Jupyter notebooks: render a compact cell listing (index/id/type/source,
|
|
1668
1746
|
// outputs summarized) so the model can find cells for notebook_edit
|
|
@@ -1763,20 +1841,40 @@ async function executeToolInner(
|
|
|
1763
1841
|
if (!fs.existsSync(filePath)) {
|
|
1764
1842
|
return attachDescriptor({ error: `File not found: ${filePath}` });
|
|
1765
1843
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1844
|
+
if (typeof args.old_string !== "string" || args.old_string === "") {
|
|
1845
|
+
return attachDescriptor({
|
|
1846
|
+
error: "old_string must be a non-empty string",
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
if (typeof args.new_string !== "string") {
|
|
1850
|
+
return attachDescriptor({ error: "new_string must be a string" });
|
|
1769
1851
|
}
|
|
1770
|
-
const
|
|
1852
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
1853
|
+
const replaceAll = args.replace_all === true;
|
|
1854
|
+
const { count, newContent } = applyLiteralEdit(
|
|
1855
|
+
content,
|
|
1771
1856
|
args.old_string,
|
|
1772
|
-
|
|
1857
|
+
args.new_string,
|
|
1858
|
+
replaceAll,
|
|
1773
1859
|
);
|
|
1860
|
+
if (count === 0) {
|
|
1861
|
+
return attachDescriptor({ error: "old_string not found in file" });
|
|
1862
|
+
}
|
|
1863
|
+
// Require a UNIQUE match (Claude-Code Edit parity) — replacing the first
|
|
1864
|
+
// of several identical strings silently edits the wrong occurrence.
|
|
1865
|
+
if (count > 1 && !replaceAll) {
|
|
1866
|
+
return attachDescriptor({
|
|
1867
|
+
error: `old_string is not unique — it appears ${count} times. Add surrounding context so it matches exactly one occurrence, or pass replace_all:true to change all of them.`,
|
|
1868
|
+
occurrences: count,
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1774
1871
|
const wrote = writeFileVerified(filePath, newContent);
|
|
1775
1872
|
if (wrote.error) return attachDescriptor({ error: wrote.error });
|
|
1776
1873
|
return attachDescriptor({
|
|
1777
1874
|
success: true,
|
|
1778
1875
|
path: filePath,
|
|
1779
1876
|
size: wrote.size,
|
|
1877
|
+
replaced: replaceAll ? count : 1,
|
|
1780
1878
|
});
|
|
1781
1879
|
}
|
|
1782
1880
|
|
|
@@ -1831,9 +1929,12 @@ async function executeToolInner(
|
|
|
1831
1929
|
}
|
|
1832
1930
|
|
|
1833
1931
|
case "run_shell": {
|
|
1834
|
-
const shellPolicyOpts =
|
|
1835
|
-
|
|
1836
|
-
|
|
1932
|
+
const shellPolicyOpts = {
|
|
1933
|
+
...(shellPolicyOverrides
|
|
1934
|
+
? { overrideRuleIds: shellPolicyOverrides }
|
|
1935
|
+
: {}),
|
|
1936
|
+
...(classifyAllShell ? { classifyAllShell: true } : {}),
|
|
1937
|
+
};
|
|
1837
1938
|
const override = getRuntimeToolDescriptorByCommand(args.command);
|
|
1838
1939
|
let shellPolicy;
|
|
1839
1940
|
let approvalOutcome = null;
|
|
@@ -1857,12 +1958,18 @@ async function executeToolInner(
|
|
|
1857
1958
|
policy: gated.policy,
|
|
1858
1959
|
};
|
|
1859
1960
|
if (!gated.allowed) {
|
|
1961
|
+
// Make a policy denial ACTIONABLE for the model (Claude-Code 2.1.193
|
|
1962
|
+
// "denial reasons to transcripts"): tell it this won't change on
|
|
1963
|
+
// retry and to involve the user, so it stops re-issuing the same
|
|
1964
|
+
// blocked command (which otherwise just burns turns + tokens).
|
|
1965
|
+
const tierLabel =
|
|
1966
|
+
typeof gated.policy === "string" ? `"${gated.policy}" ` : "";
|
|
1860
1967
|
return attachDescriptor(
|
|
1861
1968
|
{
|
|
1862
1969
|
error:
|
|
1863
1970
|
gated.via === "shell-policy"
|
|
1864
|
-
? `[Shell Policy] ${gated.reason}
|
|
1865
|
-
: `[ApprovalGate] command denied (${gated.via})
|
|
1971
|
+
? `[Shell Policy] ${gated.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`
|
|
1972
|
+
: `[ApprovalGate] command denied by the ${tierLabel}approval policy (via ${gated.via}). Retrying the same command will not help — it needs user approval. Tell the user (they can run it themselves, approve it, or relax the policy) and continue with other work.`,
|
|
1866
1973
|
shellCommandPolicy: shellPolicy,
|
|
1867
1974
|
approval: approvalOutcome,
|
|
1868
1975
|
},
|
|
@@ -1874,7 +1981,7 @@ async function executeToolInner(
|
|
|
1874
1981
|
if (!shellPolicy.allowed) {
|
|
1875
1982
|
return attachDescriptor(
|
|
1876
1983
|
{
|
|
1877
|
-
error: `[Shell Policy] ${shellPolicy.reason}
|
|
1984
|
+
error: `[Shell Policy] ${shellPolicy.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`,
|
|
1878
1985
|
shellCommandPolicy: shellPolicy,
|
|
1879
1986
|
},
|
|
1880
1987
|
override || runtimeDescriptor,
|
|
@@ -2137,12 +2244,17 @@ async function executeToolInner(
|
|
|
2137
2244
|
args: { language: args.language },
|
|
2138
2245
|
});
|
|
2139
2246
|
if (gate.decision !== APPROVAL_DECISION.ALLOW) {
|
|
2247
|
+
// Actionable denial (matches run_shell, Claude-Code 2.1.193): tell the
|
|
2248
|
+
// model retrying won't help and to involve the user.
|
|
2249
|
+
const tierLabel =
|
|
2250
|
+
typeof gate.policy === "string" ? `"${gate.policy}" ` : "";
|
|
2140
2251
|
return attachDescriptor({
|
|
2141
|
-
error: `[ApprovalGate] run_code denied (${gate.via})
|
|
2252
|
+
error: `[ApprovalGate] run_code denied by the ${tierLabel}approval policy (via ${gate.via}). Retrying won't help — running arbitrary code needs user approval. Tell the user (they can approve it or relax the policy) and continue with other work.`,
|
|
2142
2253
|
approval: {
|
|
2143
2254
|
decision: gate.decision,
|
|
2144
2255
|
via: gate.via,
|
|
2145
2256
|
riskLevel: "high",
|
|
2257
|
+
policy: gate.policy,
|
|
2146
2258
|
},
|
|
2147
2259
|
});
|
|
2148
2260
|
}
|
|
@@ -2303,13 +2415,14 @@ async function executeToolInner(
|
|
|
2303
2415
|
? [path.resolve(cwd, args.directory)]
|
|
2304
2416
|
: [cwd, ...extraRoots];
|
|
2305
2417
|
const isContent = Boolean(args.content_search);
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2418
|
+
// Pattern is model/user-supplied and flows into a shell — build the
|
|
2419
|
+
// command with it SAFELY quoted (see search-command.js). Raw interpolation
|
|
2420
|
+
// here was a command-injection bypass of the run_shell guards.
|
|
2421
|
+
const built = buildSearchCommand({ pattern: args.pattern, isContent });
|
|
2422
|
+
if (built.error) {
|
|
2423
|
+
return attachDescriptor({ error: built.error });
|
|
2424
|
+
}
|
|
2425
|
+
const cmd = built.cmd;
|
|
2313
2426
|
|
|
2314
2427
|
// Credential guard (Claude-Code 2.1.189 parity): a CONTENT search must not
|
|
2315
2428
|
// become a side channel that exfils secrets the read_file / run_shell
|
|
@@ -2326,34 +2439,34 @@ async function executeToolInner(
|
|
|
2326
2439
|
const redactedCreds = new Set();
|
|
2327
2440
|
for (const root of roots) {
|
|
2328
2441
|
if (hits.length >= 20) break;
|
|
2442
|
+
const ictx = {
|
|
2443
|
+
isContent,
|
|
2444
|
+
root,
|
|
2445
|
+
multiRoot: roots.length > 1,
|
|
2446
|
+
seen,
|
|
2447
|
+
hits,
|
|
2448
|
+
redactedCreds,
|
|
2449
|
+
credentialFileReason,
|
|
2450
|
+
};
|
|
2329
2451
|
try {
|
|
2330
2452
|
if (!fs.existsSync(root)) continue;
|
|
2331
2453
|
const output = execSync(cmd, {
|
|
2332
2454
|
cwd: root,
|
|
2333
2455
|
encoding: "utf8",
|
|
2334
2456
|
timeout: 10000,
|
|
2457
|
+
// Windows `findstr`/`dir /s` have no `head` cap (unlike the POSIX
|
|
2458
|
+
// `| head -20`), so a large tree can blow past execSync's 1 MB
|
|
2459
|
+
// default and throw ENOBUFS. Give it real headroom AND salvage the
|
|
2460
|
+
// partial below — otherwise a busy repo silently reports "no matches".
|
|
2461
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
2335
2462
|
});
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
if (credentialFileReason(src)) {
|
|
2344
|
-
redactedCreds.add(src.replace(/\\/g, "/"));
|
|
2345
|
-
seen.add(v);
|
|
2346
|
-
continue;
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
// Qualify with the root so multi-root results stay unambiguous.
|
|
2350
|
-
const labeled = roots.length > 1 ? `${root}: ${v}` : v;
|
|
2351
|
-
seen.add(v);
|
|
2352
|
-
hits.push(labeled);
|
|
2353
|
-
if (hits.length >= 20) break;
|
|
2354
|
-
}
|
|
2355
|
-
} catch {
|
|
2356
|
-
// No matches in this root — continue to the next.
|
|
2463
|
+
_ingestSearchOutput(output, ictx);
|
|
2464
|
+
} catch (err) {
|
|
2465
|
+
// A maxBuffer overflow still carries the first 8 MB of matches in
|
|
2466
|
+
// err.stdout — ingest those rather than dropping every hit as a false
|
|
2467
|
+
// "No matches found". A genuine no-match / command failure leaves
|
|
2468
|
+
// err.stdout empty, so hits stay unchanged (same as before).
|
|
2469
|
+
if (err && err.stdout) _ingestSearchOutput(err.stdout, ictx);
|
|
2357
2470
|
}
|
|
2358
2471
|
}
|
|
2359
2472
|
|
|
@@ -2376,6 +2489,12 @@ async function executeToolInner(
|
|
|
2376
2489
|
if (!fs.existsSync(dirPath)) {
|
|
2377
2490
|
return attachDescriptor({ error: `Directory not found: ${dirPath}` });
|
|
2378
2491
|
}
|
|
2492
|
+
// Clear error instead of the cryptic "ENOTDIR" readdirSync throws on a file.
|
|
2493
|
+
if (!fs.statSync(dirPath).isDirectory()) {
|
|
2494
|
+
return attachDescriptor({
|
|
2495
|
+
error: `Path is a file, not a directory: ${dirPath}. Use read_file to read it.`,
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2379
2498
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
2380
2499
|
return attachDescriptor({
|
|
2381
2500
|
entries: entries.map((e) => ({
|
|
@@ -3685,7 +3804,9 @@ const _STREAM_STALL = Symbol("stream-stall");
|
|
|
3685
3804
|
* @param {object} opts { signal?, retries?, baseDelayMs?, onRetry?, sleep? }
|
|
3686
3805
|
*/
|
|
3687
3806
|
export async function _retryStreamingChat(streamFn, opts = {}) {
|
|
3688
|
-
|
|
3807
|
+
// Default budget honors CC_MAX_RETRIES / CLAUDE_CODE_MAX_RETRIES (capped 15);
|
|
3808
|
+
// an explicit opts.retries still wins (tests / callers that pin it).
|
|
3809
|
+
const retries = opts.retries ?? resolveStreamRetryMax();
|
|
3689
3810
|
const base = opts.baseDelayMs ?? STREAM_RETRY_BASE_MS;
|
|
3690
3811
|
const signal = opts.signal;
|
|
3691
3812
|
const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
@@ -4501,6 +4622,10 @@ export async function* agentLoop(messages, options) {
|
|
|
4501
4622
|
parentMessages: messages, // pass parent messages for sub-agent auto-condensation
|
|
4502
4623
|
interaction: options.interaction || null,
|
|
4503
4624
|
shellPolicyOverrides: options.shellPolicyOverrides || null,
|
|
4625
|
+
// autoMode.classifyAllShell (Claude-Code 2.1.193): when true, the built-in
|
|
4626
|
+
// verification allowlist (npm test / rg / …) is classified through the
|
|
4627
|
+
// ApprovalGate instead of fast-pathed, so no shell command auto-runs.
|
|
4628
|
+
classifyAllShell: options.classifyAllShell || false,
|
|
4504
4629
|
approvalGate: options.approvalGate || null,
|
|
4505
4630
|
shellConfirm: options.shellConfirm || null,
|
|
4506
4631
|
// Interactive sessions (the REPL) set this so run_code is gated through the
|
|
@@ -141,19 +141,26 @@ const CODING_AGENT_TOOL_CONTRACTS = Object.freeze([
|
|
|
141
141
|
title: "Edit File",
|
|
142
142
|
kind: "filesystem",
|
|
143
143
|
tier: "mvp",
|
|
144
|
-
description:
|
|
144
|
+
description:
|
|
145
|
+
"Replace a string in a file. old_string must match EXACTLY ONE place — include enough surrounding context to make it unique, or the edit is rejected. Pass replace_all:true to change every occurrence instead.",
|
|
145
146
|
inputSchema: {
|
|
146
147
|
type: "object",
|
|
147
148
|
properties: {
|
|
148
149
|
path: { type: "string", description: "File path" },
|
|
149
150
|
old_string: {
|
|
150
151
|
type: "string",
|
|
151
|
-
description:
|
|
152
|
+
description:
|
|
153
|
+
"Exact string to replace; must be unique in the file unless replace_all is true",
|
|
152
154
|
},
|
|
153
155
|
new_string: {
|
|
154
156
|
type: "string",
|
|
155
157
|
description: "Replacement string",
|
|
156
158
|
},
|
|
159
|
+
replace_all: {
|
|
160
|
+
type: "boolean",
|
|
161
|
+
description:
|
|
162
|
+
"Replace every occurrence of old_string instead of requiring a unique match (default false)",
|
|
163
|
+
},
|
|
157
164
|
},
|
|
158
165
|
required: ["path", "old_string", "new_string"],
|
|
159
166
|
},
|