chainlesschain 0.162.160 → 0.162.162
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-ClI2ALgv.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-BDjRcJaK.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-DikSRsaT.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-9mLhMFmz.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-BSr88BrH.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-CdvyimOF.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-D3_hCedh.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-Cyvv9RHf.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-BZM14kt8.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-DrQSokVz.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-lkavIWMS.js → ChatBubbleRenderer-HwJR-C6B.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-bFLYeWMV.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-7mN7xJXT.js} +1 -1
- package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-DAB5ZWm7.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-B_1TymaK.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-Bq1uSWLJ.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-CyiE1jEe.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Cpk0D7vm.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-d_Lv3rdB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-DMqhHjMR.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-Br-oPuBw.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-CfLW_RaP.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-DZnTOExV.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-DHIUHDv1.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-Bw3t3myu.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-D9cpQPAY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Jdczr9Mr.js} +1 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-BT8ZMpC5.js +1 -0
- package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-DjARip2b.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-BgWbSRcj.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CfouJciM.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-50l7mrAr.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-rq50H84B.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BqaPmC0W.js → MarkdownRenderer-DUPIEdQH.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BYgPDmHU.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-5DVgcFdx.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-B70LPk7f.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-De8ssA5Y.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-RJsOTsIA.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-Crh-6BEK.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-wuoR5l3x.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-U2Mji4fZ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-CdTZAyDH.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-C-FucWE7.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-DCYFTeaI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-an3OF85L.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-C-1vmYFe.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-Cwh47xKg.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Dm38GT0k.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-DS2Y8awD.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BQNziA0I.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DF85L1ue.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BuLornF-.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-C3Y31IwX.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-BCPiI8aD.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BZ6s4k6g.js +1 -0
- package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-D3Wp022X.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-CUJfQkH4.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-UAc0EO8l.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-BaxKupcs.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C75NNnXZ.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BVXynZAk.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-C5MRZ0bi.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-DAKkEYka.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DvWv9YOA.js} +1 -1
- package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-B5peafW1.js} +4 -4
- package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-UQmvPEaG.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-C3bhWpuF.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-DfkKf9_H.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-DqMSjlBM.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-C-WbxWUX.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-CYuHZ4N2.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-CyeAyB9d.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-BTCeQnpq.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-BifQuA9Y.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-Bx_9Bocf.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-BX0RFCOm.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-CGHrCmUS.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-DlqaY4SO.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-DfOmj-ay.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-B72jH5fE.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-BT-63fqv.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-Dsul3sHr.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-Izkwwdgy.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-D3FHSiH1.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-CmVz5z3U.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-CuLgyJBp.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-Ml1YkXpw.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-BvjJ48bs.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-DVC0157C.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-4SBta3d7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-4wcgvPoF.js} +1 -1
- package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-B0S8NzO6.js} +1 -1
- package/src/assets/web-panel/assets/index-B7a1TrU6.js +1 -0
- package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-BUt8kTIB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-B_EvG_GW.js} +1 -1
- package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-BbPtzB8M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-BbepULSc.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-BfuKP4qe.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BiC6dQZG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-BjHBpRYZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BpFJHiTv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-BydjdZnl.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-BzG-x9p4.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-Bzr0rB5T.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-C229ABfy.js} +1 -1
- package/src/assets/web-panel/assets/index-C2B1KRja.js +1 -0
- package/src/assets/web-panel/assets/{index-BdyokEau.js → index-CFYVQMvu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-CXkJH-lK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-C_H_S-DY.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-C_XuRgGn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeBv896m.js → index-C_vRgTl9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-Ck_52dz_.js} +1 -1
- package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-CmFNvNLZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-CudmwNBV.js} +3 -3
- package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-Cw0Rjy87.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-D3lD5qap.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DC_OI9H2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DDGaOWBV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-DJyN28Et.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-DYXHFr4r.js} +1 -1
- package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-Dt5KMjwJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-DuGszaWY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DukDVXCv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-KMqhcF7l.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-KjgKdZe-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-QXzlT9KD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-vG4XSJ1d.js} +1 -1
- package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-wvb_FtaV.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-BDgd2YZ8.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-ErtrXdYe.js} +1 -1
- package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-_GPPNCgs.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-DRLL-8V2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-p86sl023.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-BTu7cMLH.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-BEKz6Isr.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-Ck42c9MP.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-1CEvuPsB.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BDfdx1GJ.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-B4K46m5w.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-DoIVpJNq.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DdAWi-ln.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-iUu6k8dx.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-DZz4R6Tw.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-DXlf9sA3.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +15 -1
- package/src/commands/agent.js +7 -0
- package/src/commands/background-session.js +16 -2
- package/src/commands/review.js +6 -1
- package/src/data/changelog.json +12 -2
- package/src/gateways/ws/background-agent-protocol.js +88 -14
- package/src/lib/agent-authority.js +251 -0
- package/src/lib/agents.js +19 -0
- package/src/lib/background-agent-phase.js +100 -0
- package/src/lib/background-agent-supervisor.js +322 -15
- package/src/lib/backpressure-policy.js +105 -0
- package/src/lib/capability-negotiation.js +212 -0
- package/src/lib/credential-guard.js +11 -0
- package/src/lib/credential-proxy.js +182 -0
- package/src/lib/docs-drift.js +166 -0
- package/src/lib/doctor-checkup.js +76 -0
- package/src/lib/event-seq-replay.js +167 -0
- package/src/lib/goal-condition-engine.js +369 -0
- package/src/lib/headless-manifest.js +13 -0
- package/src/lib/hook-event-bus.cjs +0 -0
- package/src/lib/ide-context.js +53 -14
- package/src/lib/json-schema-output.js +30 -1
- package/src/lib/json-schema-validate.js +691 -0
- package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
- package/src/lib/lsp/diagnostics-scheduler.js +127 -0
- package/src/lib/operation-fingerprint.js +95 -0
- package/src/lib/plugin-runtime/capabilities.js +371 -0
- package/src/lib/plugin-runtime/manifest.js +30 -0
- package/src/lib/project-instructions.js +98 -1
- package/src/lib/remote-approval-bridge.js +16 -0
- package/src/lib/remote-path-mapping.js +229 -0
- package/src/lib/review-pipeline.js +227 -0
- package/src/lib/runtime-checkup.js +318 -0
- package/src/lib/schedule-planner.js +136 -0
- package/src/lib/session-lifecycle.js +305 -0
- package/src/lib/settings-hook-events.cjs +39 -13
- package/src/lib/subagent-contract.js +383 -0
- package/src/lib/telemetry-ids.js +190 -0
- package/src/lib/turn-binding.js +397 -0
- package/src/repl/bg-dashboard.js +13 -12
- package/src/runtime/agent-core.js +24 -10
- package/src/runtime/headless-stream.js +195 -11
- package/src/runtime/system-prompt.js +3 -0
- package/src/workers/background-agent-worker.js +37 -2
- package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
- package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
- package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
- package/src/assets/web-panel/assets/index-x7YyhMUa.js +0 -1
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent contract — the full inheritance/override envelope for a spawned
|
|
3
|
+
* child agent (P1 "补齐 Subagent 契约").
|
|
4
|
+
*
|
|
5
|
+
* `agents.js` already parses tools/disallowedTools/model/maxTurns/isolation and
|
|
6
|
+
* agent-core's `_executeSpawnSubAgent` enforces a depth cap
|
|
7
|
+
* (`MAX_SUB_AGENT_DEPTH`) + a shared breadth counter. This module adds the REST
|
|
8
|
+
* of the Claude-Code subagent contract — `permissionMode`, `skills`,
|
|
9
|
+
* `mcpServers`, `hooks`, `memory`, `effort`, `background`,
|
|
10
|
+
* `context (fresh|fork)`, `maxDepth`/`maxChildren` and token/cost/time budgets —
|
|
11
|
+
* as PURE, deterministic functions (no timers, no RNG, no I/O):
|
|
12
|
+
*
|
|
13
|
+
* normalizeSubagentContract(raw) validate + canonicalize one declaration
|
|
14
|
+
* resolveSubagentContract(...) merge parent ⊕ definition ⊕ spawnArgs with
|
|
15
|
+
* SECURITY INVARIANTS
|
|
16
|
+
* enforceRecursionLimits(...) fail-closed depth / breadth guard
|
|
17
|
+
* resolveIsolationFailClosed(...) worktree isolation must fail, not fall back
|
|
18
|
+
*
|
|
19
|
+
* Security invariants (mirrors [[agent-authority.js]] — never trust the child to
|
|
20
|
+
* de-escalate the parent's guardrails; a child may only ever be EQUAL OR MORE
|
|
21
|
+
* RESTRICTIVE than its parent, never widen):
|
|
22
|
+
* - `permissionMode` is TIGHTEN-ONLY: a child requesting a more permissive
|
|
23
|
+
* mode than its parent is clamped to the parent's mode.
|
|
24
|
+
* - `skills`/`mcpServers`/`hooks` are INTERSECTED with the parent's set (the
|
|
25
|
+
* parent list is the ceiling; a child can never gain a capability the
|
|
26
|
+
* parent lacked). `context: fresh` withholds them entirely; only `fork`
|
|
27
|
+
* inherits when the child is silent.
|
|
28
|
+
* - `memory` access can be inherited on fork but never granted if the parent
|
|
29
|
+
* explicitly denied it.
|
|
30
|
+
* - every budget field (tokens/cost/time) is capped at the parent's REMAINING.
|
|
31
|
+
* - `maxDepth`/`maxChildren` are ceilings a child may lower but never raise.
|
|
32
|
+
*
|
|
33
|
+
* These are POLICY functions. The spawn path (`_executeSpawnSubAgent`) and the
|
|
34
|
+
* agent-file parser (`agents.js`) feed them; deeper runtime application of the
|
|
35
|
+
* resolved fields (e.g. threading `permissionMode`/`effort` into the child
|
|
36
|
+
* agentLoop) is wired incrementally.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// Public → canonical permission modes, ordered by PERMISSIVENESS (see rank).
|
|
40
|
+
// Kept in sync with headless-runner's VALID_PERMISSION_MODES.
|
|
41
|
+
export const SUBAGENT_PERMISSION_MODES = Object.freeze([
|
|
42
|
+
"plan",
|
|
43
|
+
"manual",
|
|
44
|
+
"default",
|
|
45
|
+
"acceptEdits",
|
|
46
|
+
"auto",
|
|
47
|
+
"dontAsk",
|
|
48
|
+
"bypassPermissions",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// Higher = more powerful. `plan` is read-only (most restrictive);
|
|
52
|
+
// `bypassPermissions` skips every gate (most permissive). `auto` and `dontAsk`
|
|
53
|
+
// share a tier (both auto-approve). Used only for tighten-only clamping.
|
|
54
|
+
const PERMISSION_RANK = Object.freeze({
|
|
55
|
+
plan: 0,
|
|
56
|
+
manual: 1,
|
|
57
|
+
default: 2,
|
|
58
|
+
acceptEdits: 3,
|
|
59
|
+
auto: 4,
|
|
60
|
+
dontAsk: 4,
|
|
61
|
+
bypassPermissions: 5,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const SUBAGENT_CONTEXT_MODES = Object.freeze(["fresh", "fork"]);
|
|
65
|
+
|
|
66
|
+
// Reasoning-effort levels (matches the REPL `/effort` command's vocabulary).
|
|
67
|
+
export const SUBAGENT_EFFORT_LEVELS = Object.freeze([
|
|
68
|
+
"low",
|
|
69
|
+
"medium",
|
|
70
|
+
"high",
|
|
71
|
+
"xhigh",
|
|
72
|
+
]);
|
|
73
|
+
const EFFORT_SYNONYMS = Object.freeze({ med: "medium", max: "xhigh" });
|
|
74
|
+
|
|
75
|
+
// ─── small pure helpers ─────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function toBool(v) {
|
|
78
|
+
if (v === true) return true;
|
|
79
|
+
if (v === false) return false;
|
|
80
|
+
const s = String(v).trim().toLowerCase();
|
|
81
|
+
if (s === "true" || s === "1" || s === "yes" || s === "on") return true;
|
|
82
|
+
if (s === "false" || s === "0" || s === "no" || s === "off") return false;
|
|
83
|
+
return null; // unrecognized → unspecified
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toPosInt(v) {
|
|
87
|
+
const n = Number(v);
|
|
88
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toPosNum(v) {
|
|
92
|
+
const n = Number(v);
|
|
93
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The smaller of two ceilings, treating null as "no limit from that source". */
|
|
97
|
+
function minNullable(a, b) {
|
|
98
|
+
if (a == null) return b == null ? null : b;
|
|
99
|
+
if (b == null) return a;
|
|
100
|
+
return Math.min(a, b);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Normalize a capability list (skills/mcpServers/hooks). `null`/`undefined` →
|
|
105
|
+
* `null` (UNSPECIFIED — inherit per context). A string or array → a trimmed,
|
|
106
|
+
* de-duplicated `string[]` (possibly `[]`, meaning EXPLICITLY NONE).
|
|
107
|
+
*/
|
|
108
|
+
export function normalizeCapabilityList(v) {
|
|
109
|
+
if (v == null) return null;
|
|
110
|
+
const list = Array.isArray(v) ? v : String(v).split(/[,\s]+/);
|
|
111
|
+
const out = [];
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
for (const item of list) {
|
|
114
|
+
const s = String(item).trim();
|
|
115
|
+
if (!s || seen.has(s)) continue;
|
|
116
|
+
seen.add(s);
|
|
117
|
+
out.push(s);
|
|
118
|
+
}
|
|
119
|
+
return out; // may be [] (explicit empty)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Normalize a `{tokens, cost/costUsd, time/timeMs}` budget → canonical or null. */
|
|
123
|
+
export function normalizeBudget(raw) {
|
|
124
|
+
if (!raw || typeof raw !== "object") return null;
|
|
125
|
+
const tokens = toPosNum(raw.tokens);
|
|
126
|
+
const costUsd = toPosNum(raw.costUsd ?? raw.cost);
|
|
127
|
+
const timeMs = toPosNum(raw.timeMs ?? raw.time);
|
|
128
|
+
if (tokens == null && costUsd == null && timeMs == null) return null;
|
|
129
|
+
return { tokens, costUsd, timeMs };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Permissiveness rank of a mode (unknown → `default`'s rank). */
|
|
133
|
+
export function permissionRank(mode) {
|
|
134
|
+
return PERMISSION_RANK[mode] ?? PERMISSION_RANK.default;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* TIGHTEN-ONLY permission resolution. The child's exact choice is honored when
|
|
139
|
+
* it is equal-or-more-restrictive than the parent; a widening request (or an
|
|
140
|
+
* invalid/absent one) clamps to the parent's mode. A child can never escalate.
|
|
141
|
+
*/
|
|
142
|
+
export function tightenPermissionMode(parentMode, requestedMode) {
|
|
143
|
+
const parent = SUBAGENT_PERMISSION_MODES.includes(parentMode)
|
|
144
|
+
? parentMode
|
|
145
|
+
: "default";
|
|
146
|
+
if (requestedMode == null) return parent; // silent child → inherit
|
|
147
|
+
if (!SUBAGENT_PERMISSION_MODES.includes(requestedMode)) return parent; // invalid → fail-safe
|
|
148
|
+
return permissionRank(requestedMode) <= permissionRank(parent)
|
|
149
|
+
? requestedMode
|
|
150
|
+
: parent;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── normalize one declaration ──────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate + canonicalize a raw contract declaration (agent-file frontmatter or
|
|
157
|
+
* spawn args). Unknown/invalid values are DROPPED (→ null), never thrown, so a
|
|
158
|
+
* malformed field degrades to "unspecified" rather than breaking the spawn.
|
|
159
|
+
*
|
|
160
|
+
* Every field is nullable: `null` means UNSPECIFIED (defer to inheritance),
|
|
161
|
+
* distinct from an explicit value like `false` or `[]`.
|
|
162
|
+
*/
|
|
163
|
+
export function normalizeSubagentContract(raw = {}) {
|
|
164
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
165
|
+
|
|
166
|
+
const permissionMode = SUBAGENT_PERMISSION_MODES.includes(r.permissionMode)
|
|
167
|
+
? r.permissionMode
|
|
168
|
+
: null;
|
|
169
|
+
|
|
170
|
+
const context = SUBAGENT_CONTEXT_MODES.includes(r.context) ? r.context : null;
|
|
171
|
+
|
|
172
|
+
let effort = null;
|
|
173
|
+
if (r.effort != null) {
|
|
174
|
+
const e = String(r.effort).trim().toLowerCase();
|
|
175
|
+
const canon = EFFORT_SYNONYMS[e] || e;
|
|
176
|
+
if (SUBAGENT_EFFORT_LEVELS.includes(canon)) effort = canon;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
permissionMode,
|
|
181
|
+
skills: normalizeCapabilityList(r.skills),
|
|
182
|
+
mcpServers: normalizeCapabilityList(r.mcpServers),
|
|
183
|
+
hooks: normalizeCapabilityList(r.hooks),
|
|
184
|
+
memory: r.memory == null ? null : toBool(r.memory),
|
|
185
|
+
effort,
|
|
186
|
+
background: r.background == null ? null : toBool(r.background),
|
|
187
|
+
context,
|
|
188
|
+
maxDepth: toPosInt(r.maxDepth),
|
|
189
|
+
maxChildren: toPosInt(r.maxChildren),
|
|
190
|
+
budget: normalizeBudget(r.budget),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── resolve inheritance / override ─────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/** spawnArgs ⊕ definition — the child's REQUESTED value for one field. */
|
|
197
|
+
function requested(field, definition, spawnArgs) {
|
|
198
|
+
const s = spawnArgs ? spawnArgs[field] : undefined;
|
|
199
|
+
if (s != null) return s;
|
|
200
|
+
const d = definition ? definition[field] : undefined;
|
|
201
|
+
return d != null ? d : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolve a capability list (skills/mcp/hooks) with tighten-only semantics.
|
|
206
|
+
* The parent list is the CEILING (null = "all available", no restriction).
|
|
207
|
+
* - explicit child list → INTERSECT with the parent ceiling.
|
|
208
|
+
* - silent child → inherit the parent list on `fork`; `[]` (none) on `fresh`.
|
|
209
|
+
*/
|
|
210
|
+
function resolveCapability(parentList, reqList, context) {
|
|
211
|
+
if (reqList != null) {
|
|
212
|
+
if (parentList == null) return reqList; // parent unrestricted → child's list as-is
|
|
213
|
+
const allowed = new Set(parentList);
|
|
214
|
+
return reqList.filter((x) => allowed.has(x)); // never gain what parent lacked
|
|
215
|
+
}
|
|
216
|
+
// silent child
|
|
217
|
+
if (context === "fork") return parentList == null ? null : [...parentList];
|
|
218
|
+
return []; // fresh → no inherited capabilities
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Cap a requested budget at the parent's remaining (per field; null=unlimited). */
|
|
222
|
+
export function capBudget(requestedBudget, parentRemaining) {
|
|
223
|
+
const req = requestedBudget || null;
|
|
224
|
+
const rem = parentRemaining || null;
|
|
225
|
+
if (!req && !rem) return null;
|
|
226
|
+
const cap = (a, b) => {
|
|
227
|
+
if (a == null) return b == null ? null : b;
|
|
228
|
+
if (b == null) return a;
|
|
229
|
+
return Math.min(a, b);
|
|
230
|
+
};
|
|
231
|
+
const tokens = cap(req?.tokens ?? null, rem?.tokens ?? null);
|
|
232
|
+
const costUsd = cap(req?.costUsd ?? null, rem?.costUsd ?? null);
|
|
233
|
+
const timeMs = cap(req?.timeMs ?? null, rem?.timeMs ?? null);
|
|
234
|
+
if (tokens == null && costUsd == null && timeMs == null) return null;
|
|
235
|
+
return { tokens, costUsd, timeMs };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Merge a parent's effective contract with the child's agent-file definition
|
|
240
|
+
* and per-spawn args into the child's EFFECTIVE contract, applying every
|
|
241
|
+
* security invariant. Precedence for the raw request: spawnArgs > definition;
|
|
242
|
+
* the parent then acts as the ceiling.
|
|
243
|
+
*
|
|
244
|
+
* @param {object} opts
|
|
245
|
+
* @param {object} [opts.parent] parent's EFFECTIVE contract, plus
|
|
246
|
+
* `budgetRemaining` ({tokens,costUsd,timeMs}) if the run tracks it.
|
|
247
|
+
* @param {object} [opts.definition] normalized agent-file contract (or null).
|
|
248
|
+
* @param {object} [opts.spawnArgs] normalized per-spawn contract (or null).
|
|
249
|
+
* @returns {object} effective contract (all fields resolved, never null-widening)
|
|
250
|
+
*/
|
|
251
|
+
export function resolveSubagentContract({
|
|
252
|
+
parent = {},
|
|
253
|
+
definition = null,
|
|
254
|
+
spawnArgs = null,
|
|
255
|
+
} = {}) {
|
|
256
|
+
const p = parent && typeof parent === "object" ? parent : {};
|
|
257
|
+
|
|
258
|
+
// context first — it governs how silent capability/memory fields inherit.
|
|
259
|
+
const context =
|
|
260
|
+
requested("context", definition, spawnArgs) ||
|
|
261
|
+
(SUBAGENT_CONTEXT_MODES.includes(p.context) ? p.context : "fresh");
|
|
262
|
+
|
|
263
|
+
const permissionMode = tightenPermissionMode(
|
|
264
|
+
p.permissionMode,
|
|
265
|
+
requested("permissionMode", definition, spawnArgs),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// effort is compute, not authority — inherit when the child is silent.
|
|
269
|
+
const effort = requested("effort", definition, spawnArgs) ?? p.effort ?? null;
|
|
270
|
+
|
|
271
|
+
const skills = resolveCapability(
|
|
272
|
+
p.skills ?? null,
|
|
273
|
+
requested("skills", definition, spawnArgs),
|
|
274
|
+
context,
|
|
275
|
+
);
|
|
276
|
+
const mcpServers = resolveCapability(
|
|
277
|
+
p.mcpServers ?? null,
|
|
278
|
+
requested("mcpServers", definition, spawnArgs),
|
|
279
|
+
context,
|
|
280
|
+
);
|
|
281
|
+
const hooks = resolveCapability(
|
|
282
|
+
p.hooks ?? null,
|
|
283
|
+
requested("hooks", definition, spawnArgs),
|
|
284
|
+
context,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// memory: want it explicitly, or inherit on fork if the parent had it; but a
|
|
288
|
+
// parent that explicitly denied memory can never have it granted downstream.
|
|
289
|
+
const memReq = requested("memory", definition, spawnArgs);
|
|
290
|
+
const want =
|
|
291
|
+
memReq != null ? Boolean(memReq) : context === "fork" && p.memory === true;
|
|
292
|
+
const memory = want && p.memory !== false;
|
|
293
|
+
|
|
294
|
+
const backgroundReq = requested("background", definition, spawnArgs);
|
|
295
|
+
const background = backgroundReq != null ? Boolean(backgroundReq) : false;
|
|
296
|
+
|
|
297
|
+
// depth/children ceilings — a child may lower but never raise the inherited cap.
|
|
298
|
+
const maxDepth = minNullable(
|
|
299
|
+
p.maxDepth ?? null,
|
|
300
|
+
requested("maxDepth", definition, spawnArgs),
|
|
301
|
+
);
|
|
302
|
+
const maxChildren = minNullable(
|
|
303
|
+
p.maxChildren ?? null,
|
|
304
|
+
requested("maxChildren", definition, spawnArgs),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const budget = capBudget(
|
|
308
|
+
requested("budget", definition, spawnArgs),
|
|
309
|
+
p.budgetRemaining ?? null,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
permissionMode,
|
|
314
|
+
context,
|
|
315
|
+
effort,
|
|
316
|
+
skills,
|
|
317
|
+
mcpServers,
|
|
318
|
+
hooks,
|
|
319
|
+
memory,
|
|
320
|
+
background,
|
|
321
|
+
maxDepth,
|
|
322
|
+
maxChildren,
|
|
323
|
+
budget,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── recursion guard ────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Fail-closed depth / breadth guard. `contract.maxDepth`/`maxChildren` are the
|
|
331
|
+
* child-declared ceilings; `hardDepthCap`/`hardChildrenCap` are the run's
|
|
332
|
+
* absolute backstops. The effective cap is the smaller of the two.
|
|
333
|
+
*
|
|
334
|
+
* @returns {{ok: boolean, reason: string|null}}
|
|
335
|
+
*/
|
|
336
|
+
export function enforceRecursionLimits({
|
|
337
|
+
depth = 0,
|
|
338
|
+
spawnedCount = 0,
|
|
339
|
+
contract = {},
|
|
340
|
+
hardDepthCap = null,
|
|
341
|
+
hardChildrenCap = null,
|
|
342
|
+
} = {}) {
|
|
343
|
+
const depthCap = minNullable(contract?.maxDepth ?? null, hardDepthCap);
|
|
344
|
+
if (depthCap != null && Number(depth) >= depthCap) {
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
reason: `max nesting depth (${depthCap}) reached`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const childCap = minNullable(contract?.maxChildren ?? null, hardChildrenCap);
|
|
351
|
+
if (childCap != null && Number(spawnedCount) >= childCap) {
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
reason: `max sub-agents (${childCap}) reached`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return { ok: true, reason: null };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Worktree isolation must FAIL CLOSED, never silently fall back to the parent
|
|
362
|
+
* checkout. When a contract requests `isolation: worktree` but the environment
|
|
363
|
+
* can't provide one (not a git repo, worktree add failed), the spawn is denied.
|
|
364
|
+
*
|
|
365
|
+
* @returns {{ok: boolean, isolation: string|null, reason: string|null}}
|
|
366
|
+
*/
|
|
367
|
+
export function resolveIsolationFailClosed({
|
|
368
|
+
requested: isolationRequested = null,
|
|
369
|
+
available = false,
|
|
370
|
+
} = {}) {
|
|
371
|
+
if (isolationRequested !== "worktree") {
|
|
372
|
+
return { ok: true, isolation: null, reason: null };
|
|
373
|
+
}
|
|
374
|
+
if (!available) {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
isolation: "worktree",
|
|
378
|
+
reason:
|
|
379
|
+
"worktree isolation was requested but is unavailable — refusing to run in the parent checkout",
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return { ok: true, isolation: "worktree", reason: null };
|
|
383
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified OpenTelemetry identifier + attribute normalization (P2 "可观测性").
|
|
3
|
+
*
|
|
4
|
+
* The gap: spans/events across the runtime stamp ad-hoc, inconsistent ids, so a
|
|
5
|
+
* session can't be reassembled across turns / tools / sub-agents / workflows, and
|
|
6
|
+
* nothing guarantees prompt/response/tool-argument CONTENT stays out of telemetry
|
|
7
|
+
* or that label cardinality stays bounded.
|
|
8
|
+
*
|
|
9
|
+
* This is the PURE normalization core. Given a loose context object it emits:
|
|
10
|
+
* - the canonical id set (session/turn/prompt/tool_use/agent/parent_agent/
|
|
11
|
+
* workflow.run_id/permission.decision_id/checkpoint.id) under stable keys,
|
|
12
|
+
* - CONTENT (prompt/response/tool arguments) redacted by default — opt-in only,
|
|
13
|
+
* and even then length-capped,
|
|
14
|
+
* - a bounded attribute map: only allow-listed keys survive, id values are
|
|
15
|
+
* charset-sanitized + length-capped, so a run can't explode label cardinality.
|
|
16
|
+
*
|
|
17
|
+
* Deterministic (no clock/RNG) so it's fully unit-testable; the span recorder
|
|
18
|
+
* layer feeds its raw context through here before building OTLP.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Canonical id attribute keys, mapped from the many aliases the callers use.
|
|
23
|
+
* The VALUE is the stable OTel attribute key; the KEY list are accepted aliases.
|
|
24
|
+
* Order here is the emit order (stable, low cardinality).
|
|
25
|
+
*/
|
|
26
|
+
const ID_FIELDS = Object.freeze([
|
|
27
|
+
{ key: "session.id", aliases: ["session.id", "sessionId", "session_id"] },
|
|
28
|
+
{ key: "turn.id", aliases: ["turn.id", "turnId", "turn_id"] },
|
|
29
|
+
{ key: "prompt.id", aliases: ["prompt.id", "promptId", "prompt_id"] },
|
|
30
|
+
{
|
|
31
|
+
key: "tool_use.id",
|
|
32
|
+
aliases: ["tool_use.id", "toolUseId", "tool_use_id", "toolUse.id"],
|
|
33
|
+
},
|
|
34
|
+
{ key: "agent.id", aliases: ["agent.id", "agentId", "agent_id"] },
|
|
35
|
+
{
|
|
36
|
+
key: "parent_agent.id",
|
|
37
|
+
aliases: [
|
|
38
|
+
"parent_agent.id",
|
|
39
|
+
"parentAgentId",
|
|
40
|
+
"parent_agent_id",
|
|
41
|
+
"parentAgent.id",
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "workflow.run_id",
|
|
46
|
+
aliases: ["workflow.run_id", "workflowRunId", "workflow_run_id", "runId"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "permission.decision_id",
|
|
50
|
+
aliases: [
|
|
51
|
+
"permission.decision_id",
|
|
52
|
+
"permissionDecisionId",
|
|
53
|
+
"permission_decision_id",
|
|
54
|
+
"decisionId",
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: "checkpoint.id",
|
|
59
|
+
aliases: ["checkpoint.id", "checkpointId", "checkpoint_id"],
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/** Content fields that must never be emitted unless explicitly opted in. */
|
|
64
|
+
const CONTENT_FIELDS = Object.freeze([
|
|
65
|
+
{ key: "prompt", aliases: ["prompt", "promptText", "input"] },
|
|
66
|
+
{ key: "response", aliases: ["response", "responseText", "output"] },
|
|
67
|
+
{
|
|
68
|
+
key: "tool_arguments",
|
|
69
|
+
aliases: ["tool_arguments", "toolArguments", "arguments", "args"],
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/** The complete set of allow-listed attribute keys (ids + a few low-card meta). */
|
|
74
|
+
const META_KEYS = Object.freeze([
|
|
75
|
+
"agent.kind",
|
|
76
|
+
"tool.name",
|
|
77
|
+
"workflow.name",
|
|
78
|
+
"permission.decision",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const MAX_ID_LEN = 128;
|
|
82
|
+
const MAX_CONTENT_LEN = 4096;
|
|
83
|
+
const REDACTED = "[redacted]";
|
|
84
|
+
|
|
85
|
+
function firstAlias(ctx, aliases) {
|
|
86
|
+
for (const a of aliases) {
|
|
87
|
+
if (ctx[a] != null && ctx[a] !== "") return ctx[a];
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sanitize an id VALUE to a bounded, low-cardinality-safe token: keep a stable
|
|
94
|
+
* printable charset, strip control/whitespace, cap length. Non-scalars are
|
|
95
|
+
* stringified; empties drop out (caller omits the key).
|
|
96
|
+
*/
|
|
97
|
+
export function sanitizeIdValue(value) {
|
|
98
|
+
if (value == null) return undefined;
|
|
99
|
+
let s = typeof value === "string" ? value : String(value);
|
|
100
|
+
// collapse anything outside a conservative id charset to '_'
|
|
101
|
+
s = s.replace(/[^\w.\-:/@]/g, "_").replace(/_+/g, "_");
|
|
102
|
+
s = s.replace(/^_+|_+$/g, "");
|
|
103
|
+
if (!s) return undefined;
|
|
104
|
+
return s.length > MAX_ID_LEN ? s.slice(0, MAX_ID_LEN) : s;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Redact a content value: default → the redaction sentinel; opt-in → the value
|
|
109
|
+
* stringified and length-capped (never unbounded, to keep exports sane).
|
|
110
|
+
*/
|
|
111
|
+
export function redactContent(value, includeContent = false) {
|
|
112
|
+
if (value == null) return undefined;
|
|
113
|
+
if (!includeContent) return REDACTED;
|
|
114
|
+
const s = typeof value === "string" ? value : safeStringify(value);
|
|
115
|
+
return s.length > MAX_CONTENT_LEN
|
|
116
|
+
? s.slice(0, MAX_CONTENT_LEN) + "…[truncated]"
|
|
117
|
+
: s;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function safeStringify(value) {
|
|
121
|
+
try {
|
|
122
|
+
return JSON.stringify(value);
|
|
123
|
+
} catch {
|
|
124
|
+
return String(value);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build the canonical, bounded, redacted attribute map for a telemetry context.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} ctx raw context (mixed aliases, may carry content)
|
|
132
|
+
* @param {object} [opts]
|
|
133
|
+
* includeContent — emit prompt/response/tool_arguments (still capped). Default
|
|
134
|
+
* false. This is the ONLY switch that lets content out.
|
|
135
|
+
* @returns {{ [key:string]: string }} attributes keyed by stable OTel names
|
|
136
|
+
*/
|
|
137
|
+
export function buildTelemetryAttributes(
|
|
138
|
+
ctx = {},
|
|
139
|
+
{ includeContent = false } = {},
|
|
140
|
+
) {
|
|
141
|
+
const attrs = {};
|
|
142
|
+
for (const { key, aliases } of ID_FIELDS) {
|
|
143
|
+
const raw = firstAlias(ctx, aliases);
|
|
144
|
+
const v = sanitizeIdValue(raw);
|
|
145
|
+
if (v !== undefined) attrs[key] = v;
|
|
146
|
+
}
|
|
147
|
+
for (const key of META_KEYS) {
|
|
148
|
+
const v = sanitizeIdValue(ctx[key]);
|
|
149
|
+
if (v !== undefined) attrs[key] = v;
|
|
150
|
+
}
|
|
151
|
+
for (const { key, aliases } of CONTENT_FIELDS) {
|
|
152
|
+
const raw = firstAlias(ctx, aliases);
|
|
153
|
+
if (raw == null || raw === "") continue;
|
|
154
|
+
const v = redactContent(raw, includeContent);
|
|
155
|
+
if (v !== undefined) attrs[`content.${key}`] = v;
|
|
156
|
+
}
|
|
157
|
+
return attrs;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The set of attribute keys this normalizer can ever emit (cardinality bound). */
|
|
161
|
+
export function allowedAttributeKeys() {
|
|
162
|
+
return [
|
|
163
|
+
...ID_FIELDS.map((f) => f.key),
|
|
164
|
+
...META_KEYS,
|
|
165
|
+
...CONTENT_FIELDS.map((f) => `content.${f.key}`),
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Assert an already-built attribute map only uses allow-listed keys and never
|
|
171
|
+
* leaks raw content when content wasn't opted in. Returns violation list (empty
|
|
172
|
+
* = clean) so a recorder can fail-closed in strict mode.
|
|
173
|
+
*/
|
|
174
|
+
export function auditAttributes(attrs = {}, { includeContent = false } = {}) {
|
|
175
|
+
const allowed = new Set(allowedAttributeKeys());
|
|
176
|
+
const violations = [];
|
|
177
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
178
|
+
if (!allowed.has(k)) {
|
|
179
|
+
violations.push({ key: k, reason: "key-not-allowlisted" });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (k.startsWith("content.") && !includeContent && v !== REDACTED) {
|
|
183
|
+
violations.push({ key: k, reason: "content-not-redacted" });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return violations;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const TELEMETRY_ID_KEYS = Object.freeze(ID_FIELDS.map((f) => f.key));
|
|
190
|
+
export const TELEMETRY_CONTENT_SENTINEL = REDACTED;
|