@opengeni/contracts 5.5.0 → 5.5.2-canary.40656248492001
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/dist/agent-authored-durable-text.d.ts +9 -9
- package/dist/agent-config.d.ts +148 -37
- package/dist/api-key-presets.d.ts +19 -0
- package/dist/app-action-catalog-types.d.ts +32 -0
- package/dist/app-action-catalog.gen.d.ts +2 -0
- package/dist/app-actions.d.ts +91 -0
- package/dist/app-actions.js +10130 -0
- package/dist/app-actions.js.map +1 -0
- package/dist/app-shot.d.ts +55 -0
- package/dist/artifact-catalog.d.ts +33 -0
- package/dist/artifacts.d.ts +166 -0
- package/dist/atlassian-native-retirement.d.ts +16 -0
- package/dist/atlassian-native-retirement.js +30 -0
- package/dist/atlassian-native-retirement.js.map +1 -0
- package/dist/atlassian.js +48 -23
- package/dist/automation-templates.d.ts +434 -0
- package/dist/automation-templates.js +31 -0
- package/dist/automation-templates.js.map +1 -0
- package/dist/billing-subscriptions.d.ts +61 -0
- package/dist/bots.d.ts +583 -0
- package/dist/bundled-skills.d.ts +58 -2
- package/dist/canonical-human-identities.js +3 -1
- package/dist/child-lifecycle-notice-protocol.d.ts +45 -0
- package/dist/chunk-2QKIEAGY.js +396 -0
- package/dist/chunk-2QKIEAGY.js.map +1 -0
- package/dist/chunk-2WOWEWXV.js +426 -0
- package/dist/chunk-2WOWEWXV.js.map +1 -0
- package/dist/chunk-2ZIHHL72.js +153 -0
- package/dist/chunk-2ZIHHL72.js.map +1 -0
- package/dist/chunk-32U6NKQQ.js +389 -0
- package/dist/chunk-32U6NKQQ.js.map +1 -0
- package/dist/chunk-4YDDHSO2.js +799 -0
- package/dist/chunk-4YDDHSO2.js.map +1 -0
- package/dist/chunk-56SNIMDO.js +9 -0
- package/dist/chunk-56SNIMDO.js.map +1 -0
- package/dist/chunk-5RWB6QVL.js +101 -0
- package/dist/chunk-5RWB6QVL.js.map +1 -0
- package/dist/chunk-5UGUOT3L.js +243 -0
- package/dist/chunk-5UGUOT3L.js.map +1 -0
- package/dist/chunk-6LAT33BH.js +130 -0
- package/dist/chunk-6LAT33BH.js.map +1 -0
- package/dist/chunk-7272KVWZ.js +60 -0
- package/dist/chunk-7272KVWZ.js.map +1 -0
- package/dist/chunk-72QC2GTL.js +36 -0
- package/dist/chunk-72QC2GTL.js.map +1 -0
- package/dist/chunk-7EQGGZBY.js +105 -0
- package/dist/chunk-7EQGGZBY.js.map +1 -0
- package/dist/chunk-A3LXWVYU.js +382 -0
- package/dist/chunk-A3LXWVYU.js.map +1 -0
- package/dist/chunk-AQYDZUJV.js +34 -0
- package/dist/chunk-AQYDZUJV.js.map +1 -0
- package/dist/chunk-BHCEVVUP.js +93 -0
- package/dist/chunk-BHCEVVUP.js.map +1 -0
- package/dist/chunk-BMUTAZX5.js +486 -0
- package/dist/chunk-BMUTAZX5.js.map +1 -0
- package/dist/chunk-DGG6IBBK.js +150 -0
- package/dist/chunk-DGG6IBBK.js.map +1 -0
- package/dist/{chunk-YBW5JSA3.js → chunk-FBCTUMHI.js} +123 -102
- package/dist/chunk-FBCTUMHI.js.map +1 -0
- package/dist/chunk-G2KJTFIC.js +97 -0
- package/dist/chunk-G2KJTFIC.js.map +1 -0
- package/dist/chunk-G4RKARXZ.js +1167 -0
- package/dist/chunk-G4RKARXZ.js.map +1 -0
- package/dist/chunk-H6UJJIUM.js +350 -0
- package/dist/chunk-H6UJJIUM.js.map +1 -0
- package/dist/chunk-JOTHDW4H.js +18 -0
- package/dist/chunk-JOTHDW4H.js.map +1 -0
- package/dist/chunk-L4GUEP7K.js +56480 -0
- package/dist/chunk-L4GUEP7K.js.map +1 -0
- package/dist/{chunk-SIOU2K4U.js → chunk-LJH2EYGT.js} +23 -23
- package/dist/chunk-MTJKSIGA.js +161 -0
- package/dist/chunk-MTJKSIGA.js.map +1 -0
- package/dist/chunk-MYHMB6GZ.js +202 -0
- package/dist/chunk-MYHMB6GZ.js.map +1 -0
- package/dist/chunk-PI5VIKJY.js +660 -0
- package/dist/chunk-PI5VIKJY.js.map +1 -0
- package/dist/{chunk-RIY7GYQS.js → chunk-PTSROVDG.js} +2 -2
- package/dist/chunk-PUIUXIOL.js +189 -0
- package/dist/chunk-PUIUXIOL.js.map +1 -0
- package/dist/chunk-R6VXJIMB.js +19 -0
- package/dist/chunk-R6VXJIMB.js.map +1 -0
- package/dist/chunk-RLJUNVAM.js +15 -0
- package/dist/chunk-RLJUNVAM.js.map +1 -0
- package/dist/chunk-RR6APCBK.js +213 -0
- package/dist/chunk-RR6APCBK.js.map +1 -0
- package/dist/{chunk-TXTQIYCV.js → chunk-RUQ4I6DO.js} +59 -13
- package/dist/chunk-RUQ4I6DO.js.map +1 -0
- package/dist/{chunk-WYBDERIY.js → chunk-RWEEUDHD.js} +53 -42
- package/dist/chunk-RWEEUDHD.js.map +1 -0
- package/dist/chunk-S7MSQQL6.js +158 -0
- package/dist/chunk-S7MSQQL6.js.map +1 -0
- package/dist/{chunk-5Q54VCCH.js → chunk-SHO4YO4B.js} +20 -36
- package/dist/chunk-SHO4YO4B.js.map +1 -0
- package/dist/{chunk-HHZSZYYF.js → chunk-T3HSWMA5.js} +20 -4
- package/dist/chunk-T3HSWMA5.js.map +1 -0
- package/dist/{chunk-YRKRZ6OF.js → chunk-TOOJIBJC.js} +2 -2
- package/dist/chunk-UCUOTRS6.js +431 -0
- package/dist/chunk-UCUOTRS6.js.map +1 -0
- package/dist/chunk-UH4K7GP7.js +424 -0
- package/dist/chunk-UH4K7GP7.js.map +1 -0
- package/dist/chunk-UIZWGPHV.js +120 -0
- package/dist/chunk-UIZWGPHV.js.map +1 -0
- package/dist/chunk-VLUKBV6Y.js +1170 -0
- package/dist/chunk-VLUKBV6Y.js.map +1 -0
- package/dist/chunk-W2QQ5Y6K.js +55 -0
- package/dist/chunk-W2QQ5Y6K.js.map +1 -0
- package/dist/{chunk-Y3KNXVTD.js → chunk-W3NBPR4E.js} +131 -31
- package/dist/chunk-W3NBPR4E.js.map +1 -0
- package/dist/{chunk-NRMCVHAU.js → chunk-WMGO4HJU.js} +11 -9
- package/dist/chunk-WMGO4HJU.js.map +1 -0
- package/dist/{chunk-MYBZRDTY.js → chunk-WQI7VNN4.js} +19 -9
- package/dist/chunk-WQI7VNN4.js.map +1 -0
- package/dist/{chunk-PX3VCKAU.js → chunk-WVCFSOUZ.js} +1 -1
- package/dist/{chunk-PX3VCKAU.js.map → chunk-WVCFSOUZ.js.map} +1 -1
- package/dist/chunk-XB6GQLBH.js +76 -0
- package/dist/chunk-XB6GQLBH.js.map +1 -0
- package/dist/{chunk-D2HKF2EF.js → chunk-XP5M3IKT.js} +2 -2
- package/dist/{chunk-O77VSFYZ.js → chunk-ZMPU5BWE.js} +12 -2
- package/dist/chunk-ZMPU5BWE.js.map +1 -0
- package/dist/{chunk-3SIZGL3O.js → chunk-ZYVEJFNU.js} +22 -18
- package/dist/chunk-ZYVEJFNU.js.map +1 -0
- package/dist/claude-provider-account-authority.d.ts +18 -0
- package/dist/claude-subscription-oauth.d.ts +25 -0
- package/dist/claude-subscription-usage.d.ts +95 -1
- package/dist/client-error-report.d.ts +154 -8
- package/dist/client-error-report.js +157 -2
- package/dist/client-error-report.js.map +1 -1
- package/dist/code-search.d.ts +1 -1
- package/dist/code-search.js.map +1 -1
- package/dist/codex-provider-account-authority.js +23 -12
- package/dist/codex-provider-account-authority.js.map +1 -1
- package/dist/company-profile.d.ts +144 -0
- package/dist/connect.d.ts +38 -0
- package/dist/connect.js +244 -164
- package/dist/connect.js.map +1 -1
- package/dist/connection-account-label.d.ts +2 -0
- package/dist/connection-account-label.js +34 -0
- package/dist/connection-account-label.js.map +1 -0
- package/dist/connection-authority.js +49 -24
- package/dist/connector-attachments.js +3 -1
- package/dist/connector-destinations.js +3 -1
- package/dist/connector-tool-permissions.d.ts +68 -4
- package/dist/credential-profiles.d.ts +166 -0
- package/dist/cua-desktop-tools.d.ts +19 -0
- package/dist/define-schema.d.ts +8 -0
- package/dist/desktop-app.d.ts +28 -0
- package/dist/direct-model-provider.d.ts +38 -0
- package/dist/document-artifact-commands.js +2 -2
- package/dist/document-artifact-query.js +2 -2
- package/dist/editable-artifact-binary.d.ts +7 -0
- package/dist/editable-artifact-causal-frontier.js +2 -2
- package/dist/editable-artifact-codec-registry.js +7 -7
- package/dist/editable-artifact-committed-transaction.js +4 -2
- package/dist/editable-artifact-live.js +8 -8
- package/dist/editable-artifact-serialized-commit.js +9 -9
- package/dist/editable-artifact-versions.d.ts +7 -1
- package/dist/editable-artifact-versions.js +1 -1
- package/dist/editable-artifacts.js +49 -43
- package/dist/event-preview.d.ts +22 -0
- package/dist/event-triggers.d.ts +1253 -0
- package/dist/event-triggers.js +148 -0
- package/dist/event-triggers.js.map +1 -0
- package/dist/external-identities.d.ts +34 -0
- package/dist/external-identities.js +4 -2
- package/dist/feedback.d.ts +181 -0
- package/dist/file-access.d.ts +23 -0
- package/dist/first-party-tool-authorization.d.ts +852 -0
- package/dist/github-repository-contracts.js +64 -43
- package/dist/github-repository-contracts.js.map +1 -1
- package/dist/google-drive-native-retirement.d.ts +12 -0
- package/dist/google-drive-native-retirement.js +20 -0
- package/dist/google-drive-native-retirement.js.map +1 -0
- package/dist/google-drive.d.ts +1 -1
- package/dist/google-drive.js +323 -239
- package/dist/google-drive.js.map +1 -1
- package/dist/host-mcp-bindings.js +250 -198
- package/dist/host-mcp-bindings.js.map +1 -1
- package/dist/index.d.ts +29072 -20593
- package/dist/index.js +2432 -366
- package/dist/inference-residency.d.ts +22 -0
- package/dist/insights-usage.d.ts +948 -0
- package/dist/insights-usage.js +608 -0
- package/dist/insights-usage.js.map +1 -0
- package/dist/integration-capability-id.d.ts +8 -0
- package/dist/interaction-action-deadline.d.ts +48 -0
- package/dist/interaction-driver-diagnostic.d.ts +28 -0
- package/dist/interaction.d.ts +2126 -881
- package/dist/knowledge-entries.d.ts +38 -0
- package/dist/knowledge-preparation.d.ts +4 -0
- package/dist/learning-refusal.d.ts +43 -0
- package/dist/learning-refusal.js +49 -0
- package/dist/learning-refusal.js.map +1 -0
- package/dist/managed-auth-session-sets.d.ts +1 -0
- package/dist/managed-auth-session-sets.js +207 -146
- package/dist/managed-auth-session-sets.js.map +1 -1
- package/dist/managed-sign-in-methods.d.ts +96 -0
- package/dist/managed-sign-in-methods.js +157 -35
- package/dist/managed-sign-in-methods.js.map +1 -1
- package/dist/mcp-structured-content.d.ts +25 -0
- package/dist/message-time-zone.d.ts +38 -0
- package/dist/message-time-zone.js +19 -0
- package/dist/message-time-zone.js.map +1 -0
- package/dist/modal-native-proof-v2.d.ts +587 -0
- package/dist/modal-native-proof-v2.js +39 -0
- package/dist/modal-native-proof-v2.js.map +1 -0
- package/dist/model-account-choice.d.ts +237 -0
- package/dist/model-account-settings.d.ts +422 -0
- package/dist/model-availability.d.ts +15 -0
- package/dist/model-connection-access.d.ts +13 -0
- package/dist/model-context-inspector.d.ts +8 -0
- package/dist/model-display.d.ts +43 -0
- package/dist/model-display.js +17 -0
- package/dist/model-display.js.map +1 -0
- package/dist/model-picker-order.js.map +1 -1
- package/dist/model-residency.d.ts +304 -0
- package/dist/organization-access.d.ts +416 -0
- package/dist/organization-access.js +40 -0
- package/dist/organization-access.js.map +1 -0
- package/dist/organization-integration-policy.d.ts +18 -0
- package/dist/organization-membership-lifecycle.d.ts +66 -0
- package/dist/organization-model-usage.d.ts +146 -0
- package/dist/organization-model-usage.js +87 -0
- package/dist/organization-model-usage.js.map +1 -0
- package/dist/organization-recovery.js +3 -1
- package/dist/organization-usage.d.ts +12 -0
- package/dist/passkey-only-account.d.ts +14 -0
- package/dist/passkey-only-account.js +13 -0
- package/dist/passkey-only-account.js.map +1 -0
- package/dist/permissions.d.ts +19 -0
- package/dist/personal-github.js +220 -148
- package/dist/personal-github.js.map +1 -1
- package/dist/personal-integration-setup.d.ts +81 -0
- package/dist/personal-integration-setup.js +42 -0
- package/dist/personal-integration-setup.js.map +1 -0
- package/dist/prepared-mcp-setup.d.ts +54 -0
- package/dist/prepared-mcp-setup.js +13 -0
- package/dist/prepared-mcp-setup.js.map +1 -0
- package/dist/presentation-artifact-commands.js +2 -2
- package/dist/presentation-artifact-query.js +2 -2
- package/dist/primitives.d.ts +18 -0
- package/dist/product-analytics.d.ts +27 -10
- package/dist/product-lifecycle-facts.d.ts +26 -3
- package/dist/prompt-cache-diagnostics.d.ts +92 -0
- package/dist/recordings.d.ts +1371 -0
- package/dist/remember.d.ts +98 -2
- package/dist/retained-output.d.ts +4 -2
- package/dist/sandbox-file-artifacts.d.ts +19 -2
- package/dist/sandbox-provider-command.d.ts +5 -44
- package/dist/sandbox-recovery.d.ts +139 -8
- package/dist/secret-generator.d.ts +44 -0
- package/dist/secret-mask-strings.d.ts +32 -0
- package/dist/secret-references.d.ts +90 -0
- package/dist/secret-resolution.d.ts +143 -0
- package/dist/secret-shape-masking.d.ts +52 -0
- package/dist/secret-shape-masking.js +19 -0
- package/dist/secret-shape-masking.js.map +1 -0
- package/dist/secret-totp.d.ts +41 -0
- package/dist/secret-use-approval-question.d.ts +11 -0
- package/dist/secret-use-approval-question.js +11 -0
- package/dist/secret-use-approval-question.js.map +1 -0
- package/dist/secret-use-approvals.d.ts +106 -0
- package/dist/secret-value-masking.d.ts +299 -0
- package/dist/secret-value-masking.js +62 -0
- package/dist/secret-value-masking.js.map +1 -0
- package/dist/secret-value.d.ts +19 -0
- package/dist/session-history-import.d.ts +444 -8
- package/dist/session-history.d.ts +111 -0
- package/dist/session-history.js +35 -0
- package/dist/session-history.js.map +1 -0
- package/dist/session-list-entries.d.ts +8 -0
- package/dist/session-list-entries.js +61 -0
- package/dist/session-list-entries.js.map +1 -0
- package/dist/session-mcp-projections.d.ts +13 -1
- package/dist/session-mcp-tool-permissions.d.ts +62 -0
- package/dist/session-target.d.ts +18 -0
- package/dist/session-titles.d.ts +8 -0
- package/dist/session-titles.js +5 -1
- package/dist/session-usage-limit-refusal.d.ts +34 -0
- package/dist/session-usage-limit-refusal.js +15 -0
- package/dist/session-usage-limit-refusal.js.map +1 -0
- package/dist/session-usage-limits.d.ts +127 -0
- package/dist/session-usage-limits.js +43 -0
- package/dist/session-usage-limits.js.map +1 -0
- package/dist/site-data.d.ts +286 -0
- package/dist/site-operations.d.ts +39 -0
- package/dist/site-publishing.d.ts +813 -0
- package/dist/site-session-http.d.ts +103 -4
- package/dist/site-session-http.js +27 -1
- package/dist/site-sql.d.ts +425 -0
- package/dist/skills.d.ts +38 -0
- package/dist/slack-bot-scopes.d.ts +9 -7
- package/dist/slack-bot-scopes.js +6 -3
- package/dist/slack-rest-mcp.d.ts +36 -0
- package/dist/slack-rest-mcp.js +25 -0
- package/dist/slack-rest-mcp.js.map +1 -0
- package/dist/spreadsheet-artifact-commands.d.ts +14 -1
- package/dist/spreadsheet-artifact-commands.js +9 -2
- package/dist/spreadsheet-artifact-query.d.ts +13 -6
- package/dist/spreadsheet-artifact-query.js +4 -3
- package/dist/subscription-account-actions.d.ts +83 -0
- package/dist/subscription-account-pools.d.ts +326 -0
- package/dist/subscription-personal-authority-v2.d.ts +28 -0
- package/dist/subscription-personal-authority-v2.js +13 -0
- package/dist/subscription-personal-authority-v2.js.map +1 -0
- package/dist/subscription-plans.d.ts +38 -0
- package/dist/subscription-usage-pool.d.ts +89 -0
- package/dist/tool-action-review.d.ts +141 -0
- package/dist/tool-catalog.d.ts +479 -0
- package/dist/tool-policy.d.ts +5 -0
- package/dist/tool-review-presentation.d.ts +29 -0
- package/dist/tool-review-presentation.js +21 -0
- package/dist/tool-review-presentation.js.map +1 -0
- package/dist/turn-serving-policy.d.ts +223 -0
- package/dist/usage-allowances.d.ts +30 -0
- package/dist/usage-allowances.js +5 -1
- package/dist/values.d.ts +11 -0
- package/dist/values.js +19 -0
- package/dist/values.js.map +1 -0
- package/dist/vault-provider-wire.d.ts +907 -0
- package/dist/vault-provider-wire.js +74 -0
- package/dist/vault-provider-wire.js.map +1 -0
- package/dist/vaults.d.ts +2185 -0
- package/dist/video-generation.d.ts +75 -0
- package/dist/video-generation.js +8 -1
- package/dist/workspace-control-event.d.ts +18 -0
- package/dist/workspace-control-event.js +18 -0
- package/dist/workspace-control-event.js.map +1 -0
- package/dist/workspace-governance-context.d.ts +18 -0
- package/dist/workspace-integration-wire.d.ts +1 -1
- package/dist/workspace-integration-wire.js +1 -1
- package/dist/workspace-integrations.d.ts +212 -12
- package/dist/xai-provider-account-authority.js +3 -1
- package/package.json +121 -4
- package/src/agent-authored-durable-text.ts +14 -11
- package/src/agent-config.ts +382 -203
- package/src/agent-instruction-changes.ts +94 -74
- package/src/agent-learning.ts +94 -66
- package/src/agent-topology.ts +58 -52
- package/src/api-key-presets.ts +28 -0
- package/src/app-action-catalog-types.ts +32 -0
- package/src/app-action-catalog.gen.ts +9905 -0
- package/src/app-actions.ts +365 -0
- package/src/app-shot.ts +176 -0
- package/src/artifact-catalog.ts +81 -46
- package/src/artifacts.ts +360 -227
- package/src/atlassian-native-retirement.ts +33 -0
- package/src/atlassian.ts +136 -108
- package/src/automation-templates.ts +201 -0
- package/src/billing-subscriptions.ts +48 -0
- package/src/bots.ts +238 -0
- package/src/bundled-skills.ts +79 -22
- package/src/canonical-human-identities.ts +65 -62
- package/src/child-lifecycle-notice-protocol.gen.json +1310 -0
- package/src/child-lifecycle-notice-protocol.ts +224 -0
- package/src/claude-provider-account-authority.ts +12 -0
- package/src/claude-subscription-oauth.ts +61 -19
- package/src/claude-subscription-usage.ts +64 -35
- package/src/client-error-report.ts +344 -9
- package/src/code-search.ts +1 -1
- package/src/codex-provider-account-authority.ts +22 -16
- package/src/company-brain-governed-writes.ts +265 -244
- package/src/company-brain.ts +165 -154
- package/src/company-profile.ts +362 -297
- package/src/connect.ts +222 -173
- package/src/connection-account-label.ts +45 -0
- package/src/connection-authority.ts +172 -152
- package/src/connector-attachments.ts +139 -123
- package/src/connector-destinations.ts +48 -43
- package/src/connector-tool-permissions.ts +104 -46
- package/src/credential-profiles.ts +161 -0
- package/src/cua-desktop-tools.gen.json +6722 -0
- package/src/cua-desktop-tools.linux.gen.json +6798 -0
- package/src/cua-desktop-tools.ts +42 -0
- package/src/cua-desktop-tools.windows.gen.json +6739 -0
- package/src/define-schema.ts +10 -0
- package/src/desktop-app.ts +50 -0
- package/src/direct-model-provider.ts +112 -0
- package/src/editable-artifact-binary.ts +29 -8
- package/src/editable-artifact-committed-transaction.ts +19 -18
- package/src/editable-artifact-serialized-commit.ts +8 -2
- package/src/editable-artifact-versions.ts +7 -1
- package/src/event-preview.ts +53 -3
- package/src/event-triggers.ts +891 -0
- package/src/external-identities.ts +239 -195
- package/src/feedback.ts +125 -30
- package/src/file-access.ts +26 -0
- package/src/first-party-tool-authorization.ts +383 -0
- package/src/github-repository-contracts.ts +78 -64
- package/src/google-drive-native-retirement.ts +20 -0
- package/src/google-drive.ts +286 -234
- package/src/governed-learning-activation.ts +159 -137
- package/src/governed-learning-evaluator.ts +141 -133
- package/src/host-mcp-bindings.ts +244 -221
- package/src/image-generation.ts +105 -95
- package/src/index.ts +13928 -9899
- package/src/inference-residency.ts +36 -0
- package/src/insights-usage.ts +774 -0
- package/src/integration-capability-id.ts +16 -0
- package/src/interaction-action-deadline.ts +143 -0
- package/src/interaction-driver-diagnostic.ts +89 -0
- package/src/interaction.ts +3527 -2427
- package/src/knowledge-entries.ts +386 -299
- package/src/knowledge-preparation.ts +49 -41
- package/src/knowledge.ts +242 -213
- package/src/learning-refusal.ts +90 -0
- package/src/managed-auth-session-sets.ts +180 -133
- package/src/managed-sign-in-methods.ts +156 -34
- package/src/mcp-oauth.ts +58 -46
- package/src/mcp-receipts.ts +199 -194
- package/src/mcp-structured-content.ts +81 -0
- package/src/memory-slack-delivery.ts +172 -142
- package/src/message-time-zone.ts +109 -0
- package/src/modal-native-proof-v2.ts +430 -0
- package/src/model-account-choice.ts +178 -0
- package/src/model-account-settings.ts +210 -0
- package/src/model-availability.ts +22 -0
- package/src/model-connection-access.ts +62 -14
- package/src/model-context-inspector.ts +130 -109
- package/src/model-display.ts +221 -0
- package/src/model-picker-order.ts +1 -1
- package/src/model-residency.ts +535 -0
- package/src/organization-access.ts +216 -0
- package/src/organization-integration-policy.ts +47 -28
- package/src/organization-membership-lifecycle.ts +558 -384
- package/src/organization-model-usage.ts +82 -0
- package/src/organization-recovery.ts +148 -135
- package/src/organization-usage.ts +84 -52
- package/src/passkey-only-account.ts +29 -0
- package/src/permissions.ts +94 -52
- package/src/personal-github.ts +211 -171
- package/src/personal-integration-setup.ts +150 -0
- package/src/preference-registry.ts +319 -263
- package/src/prepared-mcp-setup.ts +190 -0
- package/src/primitives.ts +22 -0
- package/src/product-analytics.ts +40 -15
- package/src/product-lifecycle-facts.ts +50 -20
- package/src/prompt-cache-diagnostics.ts +212 -0
- package/src/recordings.ts +1153 -0
- package/src/remember.ts +184 -172
- package/src/retained-output.ts +181 -168
- package/src/sandbox-file-artifacts.ts +78 -57
- package/src/sandbox-provider-command.ts +55 -72
- package/src/sandbox-recovery.ts +163 -77
- package/src/scoped-knowledge.ts +62 -43
- package/src/secret-generator.ts +156 -0
- package/src/secret-mask-strings.ts +175 -0
- package/src/secret-references.ts +414 -0
- package/src/secret-resolution.ts +322 -0
- package/src/secret-shape-masking.ts +573 -0
- package/src/secret-totp.ts +188 -0
- package/src/secret-use-approval-question.ts +22 -0
- package/src/secret-use-approvals.ts +219 -0
- package/src/secret-value-masking.ts +1283 -0
- package/src/secret-value.ts +22 -0
- package/src/session-goal-reports.ts +55 -49
- package/src/session-history-import.ts +84 -68
- package/src/session-history.ts +197 -0
- package/src/session-list-entries.ts +106 -0
- package/src/session-mcp-projections.ts +21 -3
- package/src/session-mcp-tool-permissions.ts +109 -0
- package/src/session-message-search.ts +46 -38
- package/src/session-target.ts +58 -0
- package/src/session-titles.ts +40 -4
- package/src/session-topology-primitives.ts +20 -12
- package/src/session-usage-limit-refusal.ts +101 -0
- package/src/session-usage-limits.ts +169 -0
- package/src/signup-attribution.ts +20 -15
- package/src/site-data.ts +337 -0
- package/src/site-operations.ts +154 -0
- package/src/site-publishing.ts +903 -0
- package/src/site-session-http.ts +344 -16
- package/src/site-sql.ts +1120 -0
- package/src/skill-use.ts +18 -17
- package/src/skills.ts +105 -50
- package/src/slack-bot-scopes.ts +18 -35
- package/src/slack-rest-mcp.ts +222 -0
- package/src/slack-task-policy.ts +97 -73
- package/src/spreadsheet-artifact-commands.ts +74 -13
- package/src/spreadsheet-artifact-query.ts +188 -50
- package/src/subscription-account-actions.ts +103 -0
- package/src/subscription-account-pools.ts +74 -0
- package/src/subscription-personal-authority-v2.ts +69 -0
- package/src/subscription-plans.ts +51 -0
- package/src/subscription-usage-pool.ts +44 -0
- package/src/task-notes.ts +70 -68
- package/src/tool-action-review.ts +271 -0
- package/src/tool-catalog.ts +694 -379
- package/src/tool-policy.ts +82 -0
- package/src/tool-result-spill.ts +13 -9
- package/src/tool-review-presentation.ts +643 -0
- package/src/turn-serving-policy.ts +167 -0
- package/src/usage-allowances.ts +212 -136
- package/src/values.ts +25 -0
- package/src/vault-provider-wire.ts +580 -0
- package/src/vaults.ts +1296 -0
- package/src/video-generation.ts +430 -361
- package/src/work-claims.ts +183 -166
- package/src/workspace-control-event.ts +134 -0
- package/src/workspace-governance-context.ts +63 -0
- package/src/workspace-instruction-policies.ts +346 -279
- package/src/workspace-integration-wire.ts +10 -0
- package/src/workspace-integrations.ts +488 -398
- package/src/workspace-learning-administration.ts +58 -44
- package/src/workspace-learning-policy.ts +126 -99
- package/src/workspace-state.ts +239 -220
- package/src/xai-provider-account-authority.ts +23 -17
- package/dist/chunk-3SIZGL3O.js.map +0 -1
- package/dist/chunk-566IFOVP.js +0 -26133
- package/dist/chunk-566IFOVP.js.map +0 -1
- package/dist/chunk-5IXYF6XN.js +0 -728
- package/dist/chunk-5IXYF6XN.js.map +0 -1
- package/dist/chunk-5Q54VCCH.js.map +0 -1
- package/dist/chunk-BCS7WHCK.js +0 -139
- package/dist/chunk-BCS7WHCK.js.map +0 -1
- package/dist/chunk-EK7NDGFR.js +0 -128
- package/dist/chunk-EK7NDGFR.js.map +0 -1
- package/dist/chunk-GRG7PHWQ.js +0 -25
- package/dist/chunk-GRG7PHWQ.js.map +0 -1
- package/dist/chunk-HHZSZYYF.js.map +0 -1
- package/dist/chunk-ICDZGUX2.js +0 -186
- package/dist/chunk-ICDZGUX2.js.map +0 -1
- package/dist/chunk-JUFK3T4Q.js +0 -80
- package/dist/chunk-JUFK3T4Q.js.map +0 -1
- package/dist/chunk-MYBZRDTY.js.map +0 -1
- package/dist/chunk-NRMCVHAU.js.map +0 -1
- package/dist/chunk-O77VSFYZ.js.map +0 -1
- package/dist/chunk-P3HLDT77.js +0 -59
- package/dist/chunk-P3HLDT77.js.map +0 -1
- package/dist/chunk-TXTQIYCV.js.map +0 -1
- package/dist/chunk-WYBDERIY.js.map +0 -1
- package/dist/chunk-Y3KNXVTD.js.map +0 -1
- package/dist/chunk-YBW5JSA3.js.map +0 -1
- package/dist/chunk-YRXAWWCU.js +0 -45
- package/dist/chunk-YRXAWWCU.js.map +0 -1
- package/dist/chunk-YWMOIZ6A.js +0 -185
- package/dist/chunk-YWMOIZ6A.js.map +0 -1
- package/dist/site-request-headers.test.d.ts +0 -1
- package/dist/transcription-recordings.d.ts +0 -300
- package/src/transcription-recordings.ts +0 -169
- /package/dist/{chunk-SIOU2K4U.js.map → chunk-LJH2EYGT.js.map} +0 -0
- /package/dist/{chunk-RIY7GYQS.js.map → chunk-PTSROVDG.js.map} +0 -0
- /package/dist/{chunk-YRKRZ6OF.js.map → chunk-TOOJIBJC.js.map} +0 -0
- /package/dist/{chunk-D2HKF2EF.js.map → chunk-XP5M3IKT.js.map} +0 -0
|
@@ -0,0 +1,1283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Known-value secret masking: one deterministic engine for every place that
|
|
3
|
+
* must not show a secret value it knows about (tool results, stored history
|
|
4
|
+
* and events, browser and desktop observations, history scrubbing).
|
|
5
|
+
*
|
|
6
|
+
* A value is searched for in the forms it takes in text: as written, trimmed,
|
|
7
|
+
* line by line, JSON-escaped (once and twice), percent-encoded (URI component,
|
|
8
|
+
* `+` for spaces, form encoding) and, optionally, base64 (standard and
|
|
9
|
+
* URL-safe, standalone and inside a longer base64 run). Each match is replaced
|
|
10
|
+
* by the value's label, normally the reference that resolved it
|
|
11
|
+
* (`{{secret:Vault/Item/field}}`), so whoever reads the text knows which secret
|
|
12
|
+
* it was and can keep using it by reference.
|
|
13
|
+
*
|
|
14
|
+
* Properties:
|
|
15
|
+
* - Values shorter than the minimum (8 characters by default) are never
|
|
16
|
+
* searched for: a short value such as a PIN would mangle unrelated text.
|
|
17
|
+
* - The longest form wins where several match at one position.
|
|
18
|
+
* - Idempotent: the labels and Opengeni's markers already in the text are
|
|
19
|
+
* kept, so masking masked text changes nothing. Other reference-shaped text
|
|
20
|
+
* is searched like any text: a value inside it is still the value.
|
|
21
|
+
* - A text that ends, or reaches a truncation mark (`...`, `…`), partway
|
|
22
|
+
* through a value has that clipped start replaced too, when the start is
|
|
23
|
+
* distinctive (at least 16 characters and half of the value).
|
|
24
|
+
* - Pure and isomorphic: no I/O, no clock, no randomness.
|
|
25
|
+
*
|
|
26
|
+
* Labels must be safe inside JSON strings and URLs. `secretReferenceMaskLabel`
|
|
27
|
+
* guarantees this for references; `secretCredentialMaskLabel` for Opengeni's
|
|
28
|
+
* own credentials.
|
|
29
|
+
*/
|
|
30
|
+
import { formatSecretReference, parseSecretReference } from "./secret-references";
|
|
31
|
+
import type { ResolvedSecretValue, SecretResolver } from "./secret-resolution";
|
|
32
|
+
import { isBase64DataUrl, isTextMediaType, mapSecretMaskableStrings } from "./secret-mask-strings";
|
|
33
|
+
import {
|
|
34
|
+
collectSecretShapes,
|
|
35
|
+
maskSecretShapes,
|
|
36
|
+
maskSecretShapesInText,
|
|
37
|
+
secretShapeMaskLabel,
|
|
38
|
+
type SecretShapeMatch,
|
|
39
|
+
} from "./secret-shape-masking";
|
|
40
|
+
|
|
41
|
+
export { isBase64DataUrl, isTextMediaType, mapSecretMaskableStrings };
|
|
42
|
+
|
|
43
|
+
/** Values shorter than this are not masked by default. Eight characters keeps
|
|
44
|
+
* PINs, one-time codes, ports and short words out while still covering real
|
|
45
|
+
* passwords, tokens and keys (which are almost always longer). */
|
|
46
|
+
export const SECRET_VALUE_MASK_MIN_CHARS = 8;
|
|
47
|
+
|
|
48
|
+
/** A text that ends (or reaches a truncation mark) partway through a value
|
|
49
|
+
* has that clipped start masked only when it is distinctive: at least this
|
|
50
|
+
* many characters... */
|
|
51
|
+
export const SECRET_VALUE_CLIPPED_MIN_CHARS = 16;
|
|
52
|
+
/** ...and at least this share of the value's form. A shared prefix (a URL
|
|
53
|
+
* scheme, a token's fixed header, `-----BEGIN`) is never enough on its own. */
|
|
54
|
+
export const SECRET_VALUE_CLIPPED_MIN_SHARE = 0.5;
|
|
55
|
+
|
|
56
|
+
/** Distinct values one memory keeps; the oldest drop first. */
|
|
57
|
+
export const SECRET_VALUE_MEMORY_MAX_ENTRIES = 512;
|
|
58
|
+
|
|
59
|
+
export type SecretValueMaskEntry = {
|
|
60
|
+
/** The plaintext to find. Never logged or returned by this module. */
|
|
61
|
+
value: string;
|
|
62
|
+
/** What replaces it: a reference or a fixed marker. Must be JSON- and URL-safe text. */
|
|
63
|
+
label: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type SecretValueMaskOptions = {
|
|
67
|
+
/** Values (and derived forms) shorter than this are skipped. Default 8. */
|
|
68
|
+
minChars?: number;
|
|
69
|
+
/** Also search base64 forms. Default true. */
|
|
70
|
+
base64?: boolean;
|
|
71
|
+
/** Minimum characters for a clipped start (default 16, never below `minChars`). */
|
|
72
|
+
clippedMinChars?: number;
|
|
73
|
+
/** Minimum share of the form for a clipped start, 0 to 1 (default 0.5). */
|
|
74
|
+
clippedMinShare?: number;
|
|
75
|
+
/**
|
|
76
|
+
* The end of a text counts as a cut only when the text has at least this
|
|
77
|
+
* many characters (default 1,024: output clipped to a size). Truncation
|
|
78
|
+
* marks always count.
|
|
79
|
+
*/
|
|
80
|
+
clippedTextEndMinChars?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Lines of a multi-line value searched on their own. "key-material"
|
|
83
|
+
* (default): lines of at least 16 characters made only of base64 or hex
|
|
84
|
+
* characters (PEM body lines), so a structured value's ordinary lines
|
|
85
|
+
* (`NODE_ENV=production`) stay searchable text; "all": every line of at
|
|
86
|
+
* least `minChars`.
|
|
87
|
+
*/
|
|
88
|
+
lines?: "key-material" | "all";
|
|
89
|
+
/**
|
|
90
|
+
* Which forms are indexed for clipped starts. "primary" (default): the
|
|
91
|
+
* value as written, trimmed and JSON-escaped once, which bounds memory for
|
|
92
|
+
* long values. "all": every form, including lines and percent-encoded
|
|
93
|
+
* forms (short typed values in browser and desktop results).
|
|
94
|
+
*/
|
|
95
|
+
clippedForms?: "primary" | "all";
|
|
96
|
+
/**
|
|
97
|
+
* Object keys whose string values stay untouched (identifiers, digests).
|
|
98
|
+
* Binary payloads are always skipped: a string that is entirely a base64
|
|
99
|
+
* data URL (`data:<type>/<subtype>;base64,<base64>`) and the `data` of an
|
|
100
|
+
* object that also carries a MIME type (image and file content). Payloads
|
|
101
|
+
* of a text type are searched decoded and encoded again.
|
|
102
|
+
*/
|
|
103
|
+
skipKey?: (key: string) => boolean;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type SecretValueMaskDiagnostic = {
|
|
107
|
+
/** Where masking failed, for example `tool_result` or `event_writer`. */
|
|
108
|
+
stage: string;
|
|
109
|
+
/** The error's class name only; never its message, which could carry text. */
|
|
110
|
+
error: string;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** The marker that replaces a credential Opengeni itself put into the attempt. */
|
|
114
|
+
const CREDENTIAL_LABEL_OPEN = "[opengeni credential: ";
|
|
115
|
+
/** How a `[masked <kind>]` marker (a shape value that has no reference) starts. */
|
|
116
|
+
const SHAPE_MARKER_OPEN = "[masked ";
|
|
117
|
+
|
|
118
|
+
export function secretCredentialMaskLabel(kind: string): string {
|
|
119
|
+
const safe = kind.replace(/[^A-Za-z0-9 ._-]/gu, "").slice(0, 60) || "credential";
|
|
120
|
+
return `${CREDENTIAL_LABEL_OPEN}${safe}]`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Generic replacement when no usable reference exists. */
|
|
124
|
+
export const SECRET_VALUE_GENERIC_MASK_LABEL = "[secret]";
|
|
125
|
+
|
|
126
|
+
const REFERENCE_OPEN = "\u007B\u007Bsecret:";
|
|
127
|
+
const FILE_REFERENCE_OPEN = "\u007B\u007Bsecretfile:";
|
|
128
|
+
const JSON_UNSAFE = /["\\\u0000-\u001f\u007f-\u009f\u2028\u2029]/u;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The label for a value a reference resolved to: the reference as written
|
|
132
|
+
* (a `secretfile` reference is shown as the equivalent `secret` reference,
|
|
133
|
+
* since what appears in text is the value, not the file path). A reference
|
|
134
|
+
* whose text is unsafe inside JSON (a name with a quote) is written in its ID
|
|
135
|
+
* form instead, which resolves to the same field.
|
|
136
|
+
*/
|
|
137
|
+
export function secretReferenceMaskLabel(input: {
|
|
138
|
+
raw: string;
|
|
139
|
+
itemId?: string | null;
|
|
140
|
+
fieldName?: string | null;
|
|
141
|
+
}): string {
|
|
142
|
+
let raw = input.raw;
|
|
143
|
+
if (raw.startsWith(FILE_REFERENCE_OPEN)) {
|
|
144
|
+
raw = REFERENCE_OPEN + raw.slice(FILE_REFERENCE_OPEN.length);
|
|
145
|
+
}
|
|
146
|
+
if (raw.startsWith(REFERENCE_OPEN) && !JSON_UNSAFE.test(raw)) return raw;
|
|
147
|
+
if (input.itemId) {
|
|
148
|
+
try {
|
|
149
|
+
let previous = false;
|
|
150
|
+
try {
|
|
151
|
+
previous = parseSecretReference(raw).previous;
|
|
152
|
+
} catch {
|
|
153
|
+
previous = false;
|
|
154
|
+
}
|
|
155
|
+
const id = formatSecretReference({
|
|
156
|
+
itemId: input.itemId,
|
|
157
|
+
...(input.fieldName ? { field: input.fieldName } : {}),
|
|
158
|
+
...(previous ? { previous: true } : {}),
|
|
159
|
+
});
|
|
160
|
+
if (!JSON_UNSAFE.test(id)) return id;
|
|
161
|
+
} catch {
|
|
162
|
+
// Fall through to the generic marker.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return SECRET_VALUE_GENERIC_MASK_LABEL;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function isSecretReferenceMaskLabel(label: string): boolean {
|
|
169
|
+
return label.startsWith(REFERENCE_OPEN);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Forms
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
177
|
+
|
|
178
|
+
function utf8Bytes(value: string): Uint8Array | null {
|
|
179
|
+
try {
|
|
180
|
+
return new TextEncoder().encode(value);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function base64(bytes: Uint8Array): string {
|
|
187
|
+
let out = "";
|
|
188
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
189
|
+
const a = bytes[index]!;
|
|
190
|
+
const b = index + 1 < bytes.length ? bytes[index + 1]! : 0;
|
|
191
|
+
const c = index + 2 < bytes.length ? bytes[index + 2]! : 0;
|
|
192
|
+
const triple = (a << 16) | (b << 8) | c;
|
|
193
|
+
out += BASE64_ALPHABET[(triple >> 18) & 63]!;
|
|
194
|
+
out += BASE64_ALPHABET[(triple >> 12) & 63]!;
|
|
195
|
+
out += index + 1 < bytes.length ? BASE64_ALPHABET[(triple >> 6) & 63]! : "=";
|
|
196
|
+
out += index + 2 < bytes.length ? BASE64_ALPHABET[triple & 63]! : "=";
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Standalone base64 (padded and unpadded) and the part of the encoding that
|
|
202
|
+
* depends only on the value at each of the three byte alignments a value can
|
|
203
|
+
* have inside a longer base64 run (for example `user:token` in Basic auth). */
|
|
204
|
+
function base64Forms(value: string): string[] {
|
|
205
|
+
const bytes = utf8Bytes(value);
|
|
206
|
+
if (bytes === null || bytes.length === 0) return [];
|
|
207
|
+
const forms: string[] = [];
|
|
208
|
+
const whole = base64(bytes);
|
|
209
|
+
forms.push(whole, whole.replace(/=+$/u, ""));
|
|
210
|
+
for (let offset = 0; offset < 3; offset += 1) {
|
|
211
|
+
const padded = new Uint8Array(offset + bytes.length);
|
|
212
|
+
padded.set(bytes, offset);
|
|
213
|
+
const encoded = base64(padded);
|
|
214
|
+
const start = Math.ceil((offset * 8) / 6);
|
|
215
|
+
const end = Math.floor(((offset + bytes.length) * 8) / 6);
|
|
216
|
+
if (end > start) forms.push(encoded.slice(start, end));
|
|
217
|
+
}
|
|
218
|
+
const withUrlSafe: string[] = [];
|
|
219
|
+
for (const form of forms) {
|
|
220
|
+
withUrlSafe.push(form, form.replaceAll("+", "-").replaceAll("/", "_"));
|
|
221
|
+
}
|
|
222
|
+
return withUrlSafe;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const PEM_BOUNDARY = /^-----(BEGIN|END) [A-Z0-9 ]+-----$/u;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A line of key material (a PEM body line): pure hex, or base64 characters
|
|
229
|
+
* with an upper-case letter, a lower-case letter and a digit. Not `NAME=value`
|
|
230
|
+
* and not identifier-like lines such as `production-cluster-name`.
|
|
231
|
+
*/
|
|
232
|
+
const KEY_MATERIAL_LINE =
|
|
233
|
+
/^(?:[0-9A-Fa-f]+|(?=[^\n]*[A-Z])(?=[^\n]*[a-z])(?=[^\n]*[0-9])[A-Za-z0-9+/_-]+={0,2})$/u;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Every form of one value searched for in text, longest first. Forms shorter
|
|
237
|
+
* than `minChars` are dropped; a value shorter than it has no forms.
|
|
238
|
+
*/
|
|
239
|
+
export function secretValueForms(value: string, options: SecretValueMaskOptions = {}): string[] {
|
|
240
|
+
const minChars = options.minChars ?? SECRET_VALUE_MASK_MIN_CHARS;
|
|
241
|
+
if (typeof value !== "string" || value.length < minChars) return [];
|
|
242
|
+
const forms = new Set<string>();
|
|
243
|
+
const add = (form: string | null | undefined) => {
|
|
244
|
+
if (form && form.length >= minChars) forms.add(form);
|
|
245
|
+
};
|
|
246
|
+
add(value);
|
|
247
|
+
// Tools trim the text they report and keep its first line, so a value's
|
|
248
|
+
// trimmed form and each line of a multi-line value are searched too (but
|
|
249
|
+
// not PEM boundary lines, which are the same in every key).
|
|
250
|
+
add(value.trim());
|
|
251
|
+
// By default only lines that look like key material (base64 or hex only,
|
|
252
|
+
// at least 16 characters), so a structured value's ordinary lines
|
|
253
|
+
// (`NODE_ENV=production`) stay searchable text.
|
|
254
|
+
const lines = value.split(/\r?\n|\r/u);
|
|
255
|
+
if (lines.length > 1) {
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
const trimmed = line.trim();
|
|
258
|
+
if (PEM_BOUNDARY.test(trimmed)) continue;
|
|
259
|
+
if (options.lines === "all" || (trimmed.length >= 16 && KEY_MATERIAL_LINE.test(trimmed))) {
|
|
260
|
+
add(trimmed);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const escaped = JSON.stringify(value).slice(1, -1);
|
|
265
|
+
add(escaped);
|
|
266
|
+
add(JSON.stringify(escaped).slice(1, -1));
|
|
267
|
+
try {
|
|
268
|
+
const uri = encodeURIComponent(value);
|
|
269
|
+
add(uri);
|
|
270
|
+
add(uri.replaceAll("%20", "+"));
|
|
271
|
+
add(uri.replace(/%[0-9A-F]{2}/gu, (match) => match.toLowerCase()));
|
|
272
|
+
} catch {
|
|
273
|
+
// A lone surrogate has no URI form; the raw value is still searched.
|
|
274
|
+
}
|
|
275
|
+
// Submitted forms and URLSearchParams encode more characters (for example
|
|
276
|
+
// "!" as "%21") than encodeURIComponent does.
|
|
277
|
+
try {
|
|
278
|
+
add(new URLSearchParams({ v: value }).toString().slice(2));
|
|
279
|
+
} catch {
|
|
280
|
+
// Same as above.
|
|
281
|
+
}
|
|
282
|
+
if (options.base64 !== false) for (const form of base64Forms(value)) add(form);
|
|
283
|
+
return [...forms].sort((left, right) => right.length - left.length);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Compiled masks
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Markers that are never searched inside: Opengeni's own credential and
|
|
292
|
+
* capture markers and the generic `[secret]`. A compiled set also keeps its
|
|
293
|
+
* own labels exactly. Other reference-shaped text is searched like any text:
|
|
294
|
+
* a value written inside reference syntax is still the value.
|
|
295
|
+
*/
|
|
296
|
+
const PROTECTED_PATTERNS = [
|
|
297
|
+
"\\[opengeni credential: [A-Za-z0-9 ._-]{1,60}\\]",
|
|
298
|
+
"\\[masked [a-z0-9 _-]{1,40}\\]",
|
|
299
|
+
"\\[secret\\]",
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
function escapeRegExp(text: string): string {
|
|
303
|
+
return text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Where text may have been cut: its end, and each truncation mark a tool
|
|
307
|
+
* leaves where it clipped a longer text before adding more after it. */
|
|
308
|
+
const TRUNCATION_MARK = /\.\.\.|\u2026/gu;
|
|
309
|
+
|
|
310
|
+
export type CompiledSecretValueMasks = {
|
|
311
|
+
readonly empty: boolean;
|
|
312
|
+
readonly minChars: number;
|
|
313
|
+
readonly skipKey: ((key: string) => boolean) | undefined;
|
|
314
|
+
/** Labels and markers kept as they are; null when there is nothing to search for. */
|
|
315
|
+
readonly pattern: RegExp | null;
|
|
316
|
+
readonly labels: ReadonlyMap<string, string>;
|
|
317
|
+
/** Forms longest first. */
|
|
318
|
+
readonly forms: readonly string[];
|
|
319
|
+
/** Forms by their first `minChars` characters, longest first. */
|
|
320
|
+
readonly byPrefix: ReadonlyMap<string, readonly string[]>;
|
|
321
|
+
/** The first two character codes of every form, as one number. */
|
|
322
|
+
readonly firstPairs: ReadonlySet<number>;
|
|
323
|
+
/** Clipped starts: at least this many characters... */
|
|
324
|
+
readonly clippedMinChars: number;
|
|
325
|
+
/** ...and at least this share of the form. */
|
|
326
|
+
readonly clippedMinShare: number;
|
|
327
|
+
/** The end of a text is a cut only for texts at least this long. */
|
|
328
|
+
readonly clippedTextEndMinChars: number;
|
|
329
|
+
/**
|
|
330
|
+
* Clipped starts of forms by their last `clippedMinChars` characters,
|
|
331
|
+
* longest first, so a cut needs one lookup.
|
|
332
|
+
*/
|
|
333
|
+
readonly byClippedEnd: ReadonlyMap<string, readonly ClippedStart[]>;
|
|
334
|
+
readonly longestForm: number;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/** A distinctive start of a form: `form.slice(0, length)`. */
|
|
338
|
+
type ClippedStart = { form: string; length: number };
|
|
339
|
+
|
|
340
|
+
/** Forms longer than this are not indexed for clipped starts (bounded memory). */
|
|
341
|
+
const CLIPPED_INDEX_MAX_FORM_CHARS = 8_192;
|
|
342
|
+
/**
|
|
343
|
+
* Index entries for clipped starts across all values; once reached, the
|
|
344
|
+
* remaining (longest) forms are matched whole only.
|
|
345
|
+
*/
|
|
346
|
+
const CLIPPED_INDEX_MAX_ENTRIES = 200_000;
|
|
347
|
+
/** Truncation marks examined per text, and candidates per mark. */
|
|
348
|
+
const CLIPPED_MAX_CUTS = 256;
|
|
349
|
+
const CLIPPED_MAX_CANDIDATES = 32;
|
|
350
|
+
/** The end of a text counts as a cut only for long texts (output clipped to a size). */
|
|
351
|
+
export const SECRET_VALUE_CLIPPED_TEXT_END_MIN_CHARS = 1_024;
|
|
352
|
+
|
|
353
|
+
/** Compile entries once for repeated masking. A form keeps the label of the first entry that has it. */
|
|
354
|
+
export function compileSecretValueMasks(
|
|
355
|
+
entries: readonly SecretValueMaskEntry[],
|
|
356
|
+
options: SecretValueMaskOptions = {},
|
|
357
|
+
): CompiledSecretValueMasks {
|
|
358
|
+
const minChars = Math.max(2, options.minChars ?? SECRET_VALUE_MASK_MIN_CHARS);
|
|
359
|
+
const clippedMinChars = Math.max(
|
|
360
|
+
minChars,
|
|
361
|
+
options.clippedMinChars ?? SECRET_VALUE_CLIPPED_MIN_CHARS,
|
|
362
|
+
);
|
|
363
|
+
const clippedMinShare = Math.min(
|
|
364
|
+
1,
|
|
365
|
+
Math.max(0, options.clippedMinShare ?? SECRET_VALUE_CLIPPED_MIN_SHARE),
|
|
366
|
+
);
|
|
367
|
+
const labels = new Map<string, string>();
|
|
368
|
+
const labelTexts = new Set<string>();
|
|
369
|
+
// Clipped starts are indexed for the forms text usually shows (as written,
|
|
370
|
+
// trimmed, JSON-escaped once), not the encoded variants.
|
|
371
|
+
const clippable = new Set<string>();
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
if (typeof entry?.value !== "string" || typeof entry.label !== "string") continue;
|
|
374
|
+
const forms = secretValueForms(entry.value, { ...options, minChars });
|
|
375
|
+
if (forms.length === 0) continue;
|
|
376
|
+
labelTexts.add(entry.label);
|
|
377
|
+
for (const form of forms) if (!labels.has(form)) labels.set(form, entry.label);
|
|
378
|
+
clippable.add(entry.value);
|
|
379
|
+
clippable.add(entry.value.trim());
|
|
380
|
+
clippable.add(JSON.stringify(entry.value).slice(1, -1));
|
|
381
|
+
}
|
|
382
|
+
const forms = [...labels.keys()].sort((left, right) => right.length - left.length);
|
|
383
|
+
const byPrefix = new Map<string, string[]>();
|
|
384
|
+
const byClippedEnd = new Map<string, ClippedStart[]>();
|
|
385
|
+
const firstPairs = new Set<number>();
|
|
386
|
+
for (const form of forms) {
|
|
387
|
+
const prefix = form.slice(0, minChars);
|
|
388
|
+
const bucket = byPrefix.get(prefix);
|
|
389
|
+
if (bucket) bucket.push(form);
|
|
390
|
+
else byPrefix.set(prefix, [form]);
|
|
391
|
+
firstPairs.add(form.charCodeAt(0) * 65_536 + form.charCodeAt(1));
|
|
392
|
+
}
|
|
393
|
+
// Shortest forms first, so the global cap leaves out the longest values.
|
|
394
|
+
let indexed = 0;
|
|
395
|
+
for (let index = forms.length - 1; index >= 0; index -= 1) {
|
|
396
|
+
const form = forms[index]!;
|
|
397
|
+
if (options.clippedForms !== "all" && !clippable.has(form)) continue;
|
|
398
|
+
if (form.length <= clippedMinChars || form.length > CLIPPED_INDEX_MAX_FORM_CHARS) continue;
|
|
399
|
+
const shortest = Math.max(clippedMinChars, Math.ceil(form.length * clippedMinShare));
|
|
400
|
+
if (indexed + (form.length - shortest) > CLIPPED_INDEX_MAX_ENTRIES) break;
|
|
401
|
+
for (let length = shortest; length < form.length; length += 1) {
|
|
402
|
+
const key = form.slice(length - clippedMinChars, length);
|
|
403
|
+
const starts = byClippedEnd.get(key);
|
|
404
|
+
if (starts) starts.push({ form, length });
|
|
405
|
+
else byClippedEnd.set(key, [{ form, length }]);
|
|
406
|
+
}
|
|
407
|
+
indexed += form.length - shortest;
|
|
408
|
+
}
|
|
409
|
+
for (const starts of byClippedEnd.values()) {
|
|
410
|
+
starts.sort((left, right) => right.length - left.length);
|
|
411
|
+
}
|
|
412
|
+
const pattern =
|
|
413
|
+
forms.length === 0
|
|
414
|
+
? null
|
|
415
|
+
: new RegExp(
|
|
416
|
+
[
|
|
417
|
+
...[...labelTexts].sort((left, right) => right.length - left.length).map(escapeRegExp),
|
|
418
|
+
...PROTECTED_PATTERNS,
|
|
419
|
+
]
|
|
420
|
+
.map((source) => `(?:${source})`)
|
|
421
|
+
.join("|"),
|
|
422
|
+
"gu",
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
empty: forms.length === 0,
|
|
426
|
+
minChars,
|
|
427
|
+
skipKey: options.skipKey,
|
|
428
|
+
pattern,
|
|
429
|
+
labels,
|
|
430
|
+
forms,
|
|
431
|
+
byPrefix,
|
|
432
|
+
firstPairs,
|
|
433
|
+
clippedMinChars,
|
|
434
|
+
clippedMinShare,
|
|
435
|
+
clippedTextEndMinChars: Math.max(
|
|
436
|
+
0,
|
|
437
|
+
options.clippedTextEndMinChars ?? SECRET_VALUE_CLIPPED_TEXT_END_MIN_CHARS,
|
|
438
|
+
),
|
|
439
|
+
byClippedEnd,
|
|
440
|
+
longestForm: forms[0]?.length ?? 0,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The longest start of a form that `text` ends with at `cut` (text clipped to
|
|
446
|
+
* a length can end partway through a value, and that partial value is still
|
|
447
|
+
* the secret's start). Only distinctive starts count: at least
|
|
448
|
+
* `clippedMinChars` characters and `clippedMinShare` of the form, so a
|
|
449
|
+
* shared prefix such as a URL scheme or a token's fixed header is not one.
|
|
450
|
+
*/
|
|
451
|
+
function clippedSecretTail(
|
|
452
|
+
text: string,
|
|
453
|
+
cut: number,
|
|
454
|
+
masks: CompiledSecretValueMasks,
|
|
455
|
+
): { length: number; label: string } | null {
|
|
456
|
+
const probe = masks.clippedMinChars;
|
|
457
|
+
if (cut < probe || masks.byClippedEnd.size === 0) return null;
|
|
458
|
+
const candidates = masks.byClippedEnd.get(text.slice(cut - probe, cut));
|
|
459
|
+
if (!candidates) return null;
|
|
460
|
+
// Longest first: the longest clipped value wins.
|
|
461
|
+
let checked = 0;
|
|
462
|
+
for (const { form, length } of candidates) {
|
|
463
|
+
if (length > cut) continue;
|
|
464
|
+
if (checked++ >= CLIPPED_MAX_CANDIDATES) break;
|
|
465
|
+
if (!text.startsWith(form.slice(0, length), cut - length)) continue;
|
|
466
|
+
const label = masks.labels.get(form);
|
|
467
|
+
if (label !== undefined) return { length, label };
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function overlapsKept(start: number, end: number, kept: ReadonlyArray<[number, number]>): boolean {
|
|
473
|
+
for (const [from, to] of kept) if (start < to && from < end) return true;
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function keptRanges(text: string, pattern: RegExp): Array<[number, number]> {
|
|
478
|
+
const kept: Array<[number, number]> = [];
|
|
479
|
+
pattern.lastIndex = 0;
|
|
480
|
+
for (const match of text.matchAll(pattern)) {
|
|
481
|
+
if (match[0].length > 0) kept.push([match.index, match.index + match[0].length]);
|
|
482
|
+
}
|
|
483
|
+
return kept;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Mask one text with compiled entries. */
|
|
487
|
+
export function maskSecretValuesInText(text: string, masks: CompiledSecretValueMasks): string {
|
|
488
|
+
if (masks.empty || masks.pattern === null || text.length < masks.minChars) return text;
|
|
489
|
+
const size = masks.minChars;
|
|
490
|
+
// Existing labels and markers are skipped; where one starts, it wins, and
|
|
491
|
+
// no match may run into one.
|
|
492
|
+
const kept = keptRanges(text, masks.pattern);
|
|
493
|
+
let out = "";
|
|
494
|
+
let copied = 0;
|
|
495
|
+
let next = 0;
|
|
496
|
+
for (let at = 0; at <= text.length - size;) {
|
|
497
|
+
while (next < kept.length && kept[next]![1] <= at) next += 1;
|
|
498
|
+
if (next < kept.length && kept[next]![0] <= at) {
|
|
499
|
+
at = kept[next]![1];
|
|
500
|
+
next += 1;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (!masks.firstPairs.has(text.charCodeAt(at) * 65_536 + text.charCodeAt(at + 1))) {
|
|
504
|
+
at += 1;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const candidates = masks.byPrefix.get(text.slice(at, at + size));
|
|
508
|
+
const limit = next < kept.length ? kept[next]![0] : text.length;
|
|
509
|
+
const form = candidates?.find(
|
|
510
|
+
(candidate) => at + candidate.length <= limit && text.startsWith(candidate, at),
|
|
511
|
+
);
|
|
512
|
+
if (form === undefined) {
|
|
513
|
+
at += 1;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
out += text.slice(copied, at) + masks.labels.get(form)!;
|
|
517
|
+
at += form.length;
|
|
518
|
+
copied = at;
|
|
519
|
+
}
|
|
520
|
+
out += text.slice(copied);
|
|
521
|
+
if (masks.byClippedEnd.size === 0) return out;
|
|
522
|
+
// Truncation marks (not one right after another, as in `......`), and the
|
|
523
|
+
// end of a long text. From the last cut back, so earlier positions stay valid.
|
|
524
|
+
const cuts: number[] = out.length >= masks.clippedTextEndMinChars ? [out.length] : [];
|
|
525
|
+
const marks = [...out.matchAll(TRUNCATION_MARK)];
|
|
526
|
+
for (let index = marks.length - 1; index >= 0; index -= 1) {
|
|
527
|
+
if (cuts.length >= CLIPPED_MAX_CUTS) break;
|
|
528
|
+
const mark = marks[index]!;
|
|
529
|
+
const previous = marks[index - 1];
|
|
530
|
+
if (previous && previous.index + previous[0].length === mark.index) continue;
|
|
531
|
+
cuts.push(mark.index);
|
|
532
|
+
}
|
|
533
|
+
if (cuts.length === 0) return out;
|
|
534
|
+
const keptBefore = keptRanges(out, masks.pattern);
|
|
535
|
+
let changedFrom = out.length + 1;
|
|
536
|
+
for (const cut of cuts) {
|
|
537
|
+
// A tail replaced at a later cut already covers this one.
|
|
538
|
+
if (cut > changedFrom) continue;
|
|
539
|
+
const tail = clippedSecretTail(out, cut, masks);
|
|
540
|
+
if (!tail) continue;
|
|
541
|
+
const start = cut - tail.length;
|
|
542
|
+
if (overlapsKept(start, cut, keptBefore)) continue;
|
|
543
|
+
out = out.slice(0, start) + tail.label + out.slice(cut);
|
|
544
|
+
changedFrom = start;
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Deep copy of `value` with every known secret value replaced in every string
|
|
551
|
+
* (object keys are kept). Inputs that need no change keep their identity.
|
|
552
|
+
*/
|
|
553
|
+
export function maskSecretValues<T>(
|
|
554
|
+
value: T,
|
|
555
|
+
entries: readonly SecretValueMaskEntry[] | CompiledSecretValueMasks,
|
|
556
|
+
options: SecretValueMaskOptions = {},
|
|
557
|
+
): T {
|
|
558
|
+
const masks = Array.isArray(entries)
|
|
559
|
+
? compileSecretValueMasks(entries as readonly SecretValueMaskEntry[], options)
|
|
560
|
+
: (entries as CompiledSecretValueMasks);
|
|
561
|
+
if (masks.empty) return value;
|
|
562
|
+
return mapSecretMaskableStrings(
|
|
563
|
+
value,
|
|
564
|
+
(text) => maskSecretValuesInText(text, masks),
|
|
565
|
+
masks.skipKey,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
// First-party call metadata
|
|
571
|
+
// ---------------------------------------------------------------------------
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* The MCP `_meta` key on a worker's call to Opengeni's own server that
|
|
575
|
+
* carries the values its arguments resolved to (with their references), so
|
|
576
|
+
* the server masks them in what it stores and returns. Never logged.
|
|
577
|
+
*/
|
|
578
|
+
export const FIRST_PARTY_SECRET_VALUES_META_KEY = "opengeniSecretValues";
|
|
579
|
+
|
|
580
|
+
/** The resolved values a first-party call's `_meta` carries, if any. */
|
|
581
|
+
export function firstPartySecretValuesFromMeta(meta: unknown): SecretValueMaskEntry[] {
|
|
582
|
+
if (meta === null || typeof meta !== "object") return [];
|
|
583
|
+
const entries = (meta as Record<string, unknown>)[FIRST_PARTY_SECRET_VALUES_META_KEY];
|
|
584
|
+
if (!Array.isArray(entries)) return [];
|
|
585
|
+
return entries.filter(
|
|
586
|
+
(entry): entry is SecretValueMaskEntry =>
|
|
587
|
+
entry !== null &&
|
|
588
|
+
typeof entry === "object" &&
|
|
589
|
+
typeof (entry as SecretValueMaskEntry).value === "string" &&
|
|
590
|
+
typeof (entry as SecretValueMaskEntry).label === "string",
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Whether a secret-shaped value is distinctive enough to search for in any
|
|
596
|
+
* text: at least 16 characters with three kinds of characters (lower case,
|
|
597
|
+
* upper case, digits, others), or 24 with two, and not the user name beside it.
|
|
598
|
+
*/
|
|
599
|
+
function isDistinctiveSecret(value: string, username?: string): boolean {
|
|
600
|
+
if (username !== undefined && value === username) return false;
|
|
601
|
+
const kinds =
|
|
602
|
+
Number(/[a-z]/.test(value)) +
|
|
603
|
+
Number(/[A-Z]/.test(value)) +
|
|
604
|
+
Number(/[0-9]/.test(value)) +
|
|
605
|
+
Number(/[^A-Za-z0-9]/.test(value));
|
|
606
|
+
return (value.length >= 16 && kinds >= 3) || (value.length >= 24 && kinds >= 2);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** A revealed value as it appears in text: as written and JSON-escaped. */
|
|
610
|
+
function revealedForms(value: string): string[] {
|
|
611
|
+
if (value.length === 0) return [];
|
|
612
|
+
const escaped = JSON.stringify(value).slice(1, -1);
|
|
613
|
+
return escaped === value ? [value] : [value, escaped];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
// Memory
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Saves detected secret-shaped values somewhere they can be used by
|
|
622
|
+
* reference (the task vault) and returns, per value it saved, the reference
|
|
623
|
+
* to write instead. Values it leaves out are masked with a fixed marker.
|
|
624
|
+
*/
|
|
625
|
+
export type SecretShapeCapturer = (
|
|
626
|
+
matches: readonly SecretShapeMatch[],
|
|
627
|
+
) => Promise<ReadonlyMap<string, string>>;
|
|
628
|
+
|
|
629
|
+
/** Distinct new values captured from one tool result; the rest get a marker. */
|
|
630
|
+
export const SECRET_SHAPE_CAPTURES_PER_RESULT = 16;
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* The secret values one owner (an agent attempt, a browser session) knows
|
|
634
|
+
* about, with their labels, and masking against all of them. Masking never
|
|
635
|
+
* throws: an internal failure returns the input unchanged and reports a
|
|
636
|
+
* metadata-only diagnostic.
|
|
637
|
+
*/
|
|
638
|
+
export class SecretValueMemory {
|
|
639
|
+
private readonly values = new Map<string, string>();
|
|
640
|
+
private readonly operations = new Map<string, SecretValueMaskEntry[]>();
|
|
641
|
+
private compiled: CompiledSecretValueMasks | null = null;
|
|
642
|
+
/** Masks without values that only have a `[masked <kind>]` marker, by version. */
|
|
643
|
+
private compiledReferenced: { version: number; masks: CompiledSecretValueMasks } | null = null;
|
|
644
|
+
private changes = 0;
|
|
645
|
+
private readonly options: SecretValueMaskOptions;
|
|
646
|
+
private readonly maxEntries: number;
|
|
647
|
+
private onDiagnostic: ((diagnostic: SecretValueMaskDiagnostic) => void) | undefined;
|
|
648
|
+
private refresher: (() => Promise<void>) | undefined;
|
|
649
|
+
/** Values an explicit, audited reveal (`secret_get`) handed to the agent. */
|
|
650
|
+
private readonly revealed = new Set<string>();
|
|
651
|
+
private disposed = false;
|
|
652
|
+
/** Secret-shaped values seen in tool output, with what replaces them (any length). */
|
|
653
|
+
private readonly shapes = new Map<string, string>();
|
|
654
|
+
private shapeCapturer: SecretShapeCapturer | undefined;
|
|
655
|
+
|
|
656
|
+
constructor(
|
|
657
|
+
options: SecretValueMaskOptions & {
|
|
658
|
+
maxEntries?: number;
|
|
659
|
+
onDiagnostic?: (diagnostic: SecretValueMaskDiagnostic) => void;
|
|
660
|
+
} = {},
|
|
661
|
+
) {
|
|
662
|
+
const { maxEntries, onDiagnostic, ...maskOptions } = options;
|
|
663
|
+
this.options = maskOptions;
|
|
664
|
+
this.maxEntries = maxEntries ?? SECRET_VALUE_MEMORY_MAX_ENTRIES;
|
|
665
|
+
this.onDiagnostic = onDiagnostic;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Replace the diagnostic sink (for example once observability is ready). */
|
|
669
|
+
setDiagnosticSink(sink: ((diagnostic: SecretValueMaskDiagnostic) => void) | undefined): void {
|
|
670
|
+
this.onDiagnostic = sink;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Attach a loader that may remember more values (for example uses recorded
|
|
675
|
+
* by another process). It owns its own throttling.
|
|
676
|
+
*/
|
|
677
|
+
setRefresher(refresher: (() => Promise<void>) | undefined): void {
|
|
678
|
+
this.refresher = refresher;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Run the refresher, waiting at most `maxWaitMs`; never throws. */
|
|
682
|
+
async refresh(maxWaitMs = 2_000): Promise<void> {
|
|
683
|
+
const refresher = this.refresher;
|
|
684
|
+
if (!refresher) return;
|
|
685
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
686
|
+
try {
|
|
687
|
+
await Promise.race([
|
|
688
|
+
refresher(),
|
|
689
|
+
new Promise<void>((resolve) => {
|
|
690
|
+
timer = setTimeout(resolve, maxWaitMs);
|
|
691
|
+
}),
|
|
692
|
+
]);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
this.report("refresh", error);
|
|
695
|
+
} finally {
|
|
696
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
get minChars(): number {
|
|
701
|
+
return this.options.minChars ?? SECRET_VALUE_MASK_MIN_CHARS;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
get size(): number {
|
|
705
|
+
return this.values.size;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Increases whenever the searched values change (for memoized masking). */
|
|
709
|
+
get version(): number {
|
|
710
|
+
return this.changes;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Remember a value. Returns whether it is searched for (false when shorter
|
|
715
|
+
* than the minimum). A value keeps its first reference label; a reference
|
|
716
|
+
* label replaces a credential marker, because it is the more useful name.
|
|
717
|
+
*/
|
|
718
|
+
remember(value: string, label: string): boolean {
|
|
719
|
+
try {
|
|
720
|
+
if (typeof value !== "string" || typeof label !== "string") return false;
|
|
721
|
+
if (this.disposed || value.length < this.minChars || this.isRevealed(value)) return false;
|
|
722
|
+
const existing = this.values.get(value);
|
|
723
|
+
const keep =
|
|
724
|
+
existing !== undefined &&
|
|
725
|
+
(existing === label ||
|
|
726
|
+
isSecretReferenceMaskLabel(existing) ||
|
|
727
|
+
!isSecretReferenceMaskLabel(label));
|
|
728
|
+
this.values.delete(value);
|
|
729
|
+
this.values.set(value, keep ? existing! : label);
|
|
730
|
+
this.evict();
|
|
731
|
+
if (!keep) {
|
|
732
|
+
this.compiled = null;
|
|
733
|
+
this.changes += 1;
|
|
734
|
+
}
|
|
735
|
+
return true;
|
|
736
|
+
} catch (error) {
|
|
737
|
+
this.report("remember", error);
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Over the limit, the oldest values go first, but Opengeni's own
|
|
744
|
+
* credentials (`[opengeni credential: …]`) stay while others can go.
|
|
745
|
+
*/
|
|
746
|
+
private evict(): void {
|
|
747
|
+
if (this.values.size <= this.maxEntries) return;
|
|
748
|
+
for (const [value, label] of this.values) {
|
|
749
|
+
if (this.values.size <= this.maxEntries) break;
|
|
750
|
+
if (label.startsWith(CREDENTIAL_LABEL_OPEN)) continue;
|
|
751
|
+
this.values.delete(value);
|
|
752
|
+
}
|
|
753
|
+
while (this.values.size > this.maxEntries) {
|
|
754
|
+
const oldest = this.values.keys().next().value;
|
|
755
|
+
if (oldest === undefined) break;
|
|
756
|
+
this.values.delete(oldest);
|
|
757
|
+
}
|
|
758
|
+
this.compiled = null;
|
|
759
|
+
this.changes += 1;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* An explicit, audited plaintext read (`secret_get`) handed this value to
|
|
764
|
+
* the agent on purpose: it is not masked for the rest of this memory's
|
|
765
|
+
* life. Never throws.
|
|
766
|
+
*/
|
|
767
|
+
reveal(value: string): void {
|
|
768
|
+
try {
|
|
769
|
+
if (typeof value !== "string" || value.length === 0) return;
|
|
770
|
+
this.revealed.add(value);
|
|
771
|
+
// A value inside the revealed one (the password of a revealed URL) was
|
|
772
|
+
// handed over with it, so it is released too; Opengeni's own
|
|
773
|
+
// credentials stay masked.
|
|
774
|
+
let changed = false;
|
|
775
|
+
for (const [known, label] of this.values) {
|
|
776
|
+
if (label.startsWith(CREDENTIAL_LABEL_OPEN) || !value.includes(known)) continue;
|
|
777
|
+
this.values.delete(known);
|
|
778
|
+
changed = true;
|
|
779
|
+
}
|
|
780
|
+
for (const known of this.shapes.keys()) {
|
|
781
|
+
if (value.includes(known)) this.shapes.delete(known);
|
|
782
|
+
}
|
|
783
|
+
if (changed) {
|
|
784
|
+
this.compiled = null;
|
|
785
|
+
this.changes += 1;
|
|
786
|
+
}
|
|
787
|
+
} catch (error) {
|
|
788
|
+
this.report("reveal", error);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Whether `value` is, or lies within, a value `secret_get` revealed. Only a
|
|
794
|
+
* value of at least the minimum length counts as lying within one, so a
|
|
795
|
+
* short value is not released by a chance match.
|
|
796
|
+
*/
|
|
797
|
+
private isRevealed(value: string): boolean {
|
|
798
|
+
if (this.revealed.size === 0) return false;
|
|
799
|
+
if (this.revealed.has(value)) return true;
|
|
800
|
+
if (value.length < this.minChars) return false;
|
|
801
|
+
for (const revealed of this.revealed) if (revealed.includes(value)) return true;
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Forget every value at the end of the owner's life (an attempt), so work
|
|
807
|
+
* that outlives it neither keeps the plaintext nor masks with it.
|
|
808
|
+
*/
|
|
809
|
+
dispose(): void {
|
|
810
|
+
this.disposed = true;
|
|
811
|
+
this.values.clear();
|
|
812
|
+
this.operations.clear();
|
|
813
|
+
this.revealed.clear();
|
|
814
|
+
this.shapes.clear();
|
|
815
|
+
this.shapeCapturer = undefined;
|
|
816
|
+
this.refresher = undefined;
|
|
817
|
+
this.compiled = null;
|
|
818
|
+
this.compiledReferenced = null;
|
|
819
|
+
this.changes += 1;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Remember a value and note that one tool call (its operation ID) used it,
|
|
824
|
+
* so the receiver of that call can mask its own stored text with exactly
|
|
825
|
+
* the values it was given.
|
|
826
|
+
*/
|
|
827
|
+
rememberForOperation(operationId: string, value: string, label: string): boolean {
|
|
828
|
+
const searched = this.remember(value, label);
|
|
829
|
+
if (!searched || !operationId) return searched;
|
|
830
|
+
try {
|
|
831
|
+
const entries = this.operations.get(operationId) ?? [];
|
|
832
|
+
if (!entries.some((entry) => entry.value === value)) entries.push({ value, label });
|
|
833
|
+
this.operations.delete(operationId);
|
|
834
|
+
this.operations.set(operationId, entries);
|
|
835
|
+
while (this.operations.size > 256) {
|
|
836
|
+
const oldest = this.operations.keys().next().value;
|
|
837
|
+
if (oldest === undefined) break;
|
|
838
|
+
this.operations.delete(oldest);
|
|
839
|
+
}
|
|
840
|
+
} catch (error) {
|
|
841
|
+
this.report("remember", error);
|
|
842
|
+
}
|
|
843
|
+
return searched;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** The values one tool call used (see `rememberForOperation`). */
|
|
847
|
+
operationEntries(operationId: string): SecretValueMaskEntry[] {
|
|
848
|
+
return [...(this.operations.get(operationId) ?? [])];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Whether a value is remembered (for tests and dedupe; never exposes values). */
|
|
852
|
+
has(value: string): boolean {
|
|
853
|
+
return this.values.has(value);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
entries(): SecretValueMaskEntry[] {
|
|
857
|
+
return [...this.values].map(([value, label]) => ({ value, label }));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
private masks(): CompiledSecretValueMasks {
|
|
861
|
+
this.compiled ??= compileSecretValueMasks(this.entries(), this.options);
|
|
862
|
+
return this.compiled;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Deep copy with every remembered value replaced; never throws. */
|
|
866
|
+
mask<T>(value: T, stage = "mask"): T {
|
|
867
|
+
if (this.values.size === 0) return value;
|
|
868
|
+
try {
|
|
869
|
+
return maskSecretValues(value, this.masks());
|
|
870
|
+
} catch (error) {
|
|
871
|
+
this.report(stage, error);
|
|
872
|
+
return value;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** `mask` without the values whose label is only a `[masked <kind>]` marker. */
|
|
877
|
+
private maskReferenced<T>(value: T, stage: string): T {
|
|
878
|
+
if (this.values.size === 0) return value;
|
|
879
|
+
let markers = false;
|
|
880
|
+
for (const label of this.values.values()) {
|
|
881
|
+
if (label.startsWith(SHAPE_MARKER_OPEN)) {
|
|
882
|
+
markers = true;
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (!markers) return this.mask(value, stage);
|
|
887
|
+
try {
|
|
888
|
+
if (this.compiledReferenced?.version !== this.changes) {
|
|
889
|
+
this.compiledReferenced = {
|
|
890
|
+
version: this.changes,
|
|
891
|
+
masks: compileSecretValueMasks(
|
|
892
|
+
this.entries().filter((entry) => !entry.label.startsWith(SHAPE_MARKER_OPEN)),
|
|
893
|
+
this.options,
|
|
894
|
+
),
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
return maskSecretValues(value, this.compiledReferenced.masks);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
this.report(stage, error);
|
|
900
|
+
return value;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Mask one text; never throws. */
|
|
905
|
+
maskText(text: string, stage = "mask"): string {
|
|
906
|
+
if (this.values.size === 0 || typeof text !== "string") return text;
|
|
907
|
+
try {
|
|
908
|
+
return maskSecretValuesInText(text, this.masks());
|
|
909
|
+
} catch (error) {
|
|
910
|
+
this.report(stage, error);
|
|
911
|
+
return text;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** Mask with the remembered values plus, for this call only, extra ones. */
|
|
916
|
+
maskWith<T>(value: T, extra: readonly SecretValueMaskEntry[], stage = "mask"): T {
|
|
917
|
+
if (extra.length === 0) return this.mask(value, stage);
|
|
918
|
+
try {
|
|
919
|
+
return maskSecretValues(value, [...this.entries(), ...extra], this.options);
|
|
920
|
+
} catch (error) {
|
|
921
|
+
this.report(stage, error);
|
|
922
|
+
return value;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Attach the capturer that saves secret-shaped values found in tool output
|
|
928
|
+
* (normally into the task vault). Without one they are masked with a marker.
|
|
929
|
+
*/
|
|
930
|
+
setShapeCapturer(capturer: SecretShapeCapturer | undefined): void {
|
|
931
|
+
this.shapeCapturer = capturer;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Remember a secret-shaped value from tool output: it is replaced wherever
|
|
936
|
+
* a shape detector finds it, and (when long enough) searched for as a known
|
|
937
|
+
* value everywhere else. A reference replaces an earlier marker.
|
|
938
|
+
*/
|
|
939
|
+
rememberShape(value: string, label: string, username?: string): void {
|
|
940
|
+
try {
|
|
941
|
+
if (typeof value !== "string" || typeof label !== "string" || value.length === 0) return;
|
|
942
|
+
// An ended attempt keeps nothing; a value `secret_get` revealed stays readable.
|
|
943
|
+
if (this.disposed || this.isRevealed(value)) return;
|
|
944
|
+
const existing = this.shapes.get(value);
|
|
945
|
+
if (
|
|
946
|
+
existing === undefined ||
|
|
947
|
+
(!isSecretReferenceMaskLabel(existing) && isSecretReferenceMaskLabel(label))
|
|
948
|
+
) {
|
|
949
|
+
this.shapes.set(value, label);
|
|
950
|
+
while (this.shapes.size > this.maxEntries) {
|
|
951
|
+
const oldest = this.shapes.keys().next().value;
|
|
952
|
+
if (oldest === undefined) break;
|
|
953
|
+
this.shapes.delete(oldest);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// Searched everywhere else only when it is distinctive: a weak or
|
|
957
|
+
// common password (`postgres`, the URL's own user name) would mask
|
|
958
|
+
// ordinary words. Where a detector finds it, it is masked regardless.
|
|
959
|
+
if (isDistinctiveSecret(value, username)) {
|
|
960
|
+
this.remember(value, this.shapes.get(value) ?? label);
|
|
961
|
+
}
|
|
962
|
+
} catch (error) {
|
|
963
|
+
this.report("remember_shape", error);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Find secret-shaped values in tool output that are not known yet and
|
|
969
|
+
* capture them, waiting at most `maxWaitMs`. Values the capturer does not
|
|
970
|
+
* return a reference for (or all of them, without a capturer or on any
|
|
971
|
+
* failure) are remembered with a `[masked <kind>]` marker. Never throws.
|
|
972
|
+
*/
|
|
973
|
+
async captureShapes(value: unknown, stage = "tool_result", maxWaitMs = 5_000): Promise<void> {
|
|
974
|
+
let pending: SecretShapeMatch[];
|
|
975
|
+
try {
|
|
976
|
+
// Known values are masked first (a detected part of a vault secret is
|
|
977
|
+
// not captured again), except values that only have a marker: those
|
|
978
|
+
// are detected again, so a failed or skipped capture is retried.
|
|
979
|
+
const known = this.maskReferenced(value, stage);
|
|
980
|
+
// Values that already have a reference are done; markers are retried.
|
|
981
|
+
pending = collectSecretShapes(known, 256, {
|
|
982
|
+
...(this.options.skipKey ? { skipKey: this.options.skipKey } : {}),
|
|
983
|
+
keep: (match, text) => !this.insideRevealed(match, text) && !this.isRevealed(match.value),
|
|
984
|
+
}).filter((match) => !isSecretReferenceMaskLabel(this.shapes.get(match.value) ?? ""));
|
|
985
|
+
} catch (error) {
|
|
986
|
+
this.report(stage, error);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (pending.length === 0) return;
|
|
990
|
+
const capturable = pending.slice(0, SECRET_SHAPE_CAPTURES_PER_RESULT);
|
|
991
|
+
const usernames = new Map(pending.map((match) => [match.value, match.username]));
|
|
992
|
+
let captured: ReadonlyMap<string, string> = new Map();
|
|
993
|
+
const capturer = this.shapeCapturer;
|
|
994
|
+
if (capturer) {
|
|
995
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
996
|
+
try {
|
|
997
|
+
const late = capturer(capturable);
|
|
998
|
+
// A capture that finishes after the wait still upgrades the markers.
|
|
999
|
+
late.then(
|
|
1000
|
+
(references) => {
|
|
1001
|
+
for (const [secret, reference] of references) {
|
|
1002
|
+
if (!isSecretReferenceMaskLabel(reference)) continue;
|
|
1003
|
+
this.rememberShape(secret, reference, usernames.get(secret));
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
() => {},
|
|
1007
|
+
);
|
|
1008
|
+
captured =
|
|
1009
|
+
(await Promise.race([
|
|
1010
|
+
late,
|
|
1011
|
+
new Promise<null>((resolve) => {
|
|
1012
|
+
timer = setTimeout(() => resolve(null), maxWaitMs);
|
|
1013
|
+
}),
|
|
1014
|
+
])) ?? new Map();
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
this.report("shape_capture", error);
|
|
1017
|
+
} finally {
|
|
1018
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
for (const match of pending) {
|
|
1022
|
+
const reference = captured.get(match.value);
|
|
1023
|
+
this.rememberShape(
|
|
1024
|
+
match.value,
|
|
1025
|
+
reference && isSecretReferenceMaskLabel(reference)
|
|
1026
|
+
? reference
|
|
1027
|
+
: secretShapeMaskLabel(match.kind),
|
|
1028
|
+
match.username,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Whether a detected part lies within a value `secret_get` revealed (as
|
|
1035
|
+
* written or JSON-escaped), for example the password of a revealed URL.
|
|
1036
|
+
*/
|
|
1037
|
+
private insideRevealed(match: SecretShapeMatch, text: string): boolean {
|
|
1038
|
+
if (this.revealed.size === 0) return false;
|
|
1039
|
+
for (const value of this.revealed) {
|
|
1040
|
+
for (const form of revealedForms(value)) {
|
|
1041
|
+
const at = text.indexOf(form, Math.max(0, match.start - form.length + 1));
|
|
1042
|
+
if (at >= 0 && at < match.end) return true;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** What replaces a detected shape: its capture, a known label, a marker, or nothing when revealed. */
|
|
1049
|
+
private shapeLabel = (match: SecretShapeMatch, text: string): string | null =>
|
|
1050
|
+
this.insideRevealed(match, text) || this.isRevealed(match.value)
|
|
1051
|
+
? null
|
|
1052
|
+
: (this.shapes.get(match.value) ??
|
|
1053
|
+
this.values.get(match.value) ??
|
|
1054
|
+
secretShapeMaskLabel(match.kind));
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Mask tool output: known values first, then every secret-shaped part
|
|
1058
|
+
* (with its captured reference, or a marker). Never throws.
|
|
1059
|
+
*/
|
|
1060
|
+
maskToolOutput<T>(value: T, stage = "tool_result"): T {
|
|
1061
|
+
const known = this.mask(value, stage);
|
|
1062
|
+
try {
|
|
1063
|
+
return maskSecretShapes(known, this.shapeLabel, this.options.skipKey);
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
this.report(stage, error);
|
|
1066
|
+
return known;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/** `maskToolOutput` for one text. */
|
|
1071
|
+
maskToolOutputText(text: string, stage = "tool_result"): string {
|
|
1072
|
+
const known = this.maskText(text, stage);
|
|
1073
|
+
if (typeof known !== "string") return known;
|
|
1074
|
+
try {
|
|
1075
|
+
return maskSecretShapesInText(known, this.shapeLabel);
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
this.report(stage, error);
|
|
1078
|
+
return known;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private report(stage: string, error: unknown): void {
|
|
1083
|
+
try {
|
|
1084
|
+
this.onDiagnostic?.({ stage, error: error instanceof Error ? error.name : typeof error });
|
|
1085
|
+
} catch {
|
|
1086
|
+
// Diagnostics never affect masking.
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Mask an error's message, stack, causes and inner errors (`AggregateError`)
|
|
1093
|
+
* in place with `memory` (bounded depth) and return it, so a rethrown error
|
|
1094
|
+
* carries references, not values. In "tool_output" mode secret-shaped parts
|
|
1095
|
+
* are masked too.
|
|
1096
|
+
* Never throws: on failure the original error is returned as it is.
|
|
1097
|
+
*/
|
|
1098
|
+
export function maskSecretValuesInError<E>(
|
|
1099
|
+
error: E,
|
|
1100
|
+
memory: SecretValueMemory | null | undefined,
|
|
1101
|
+
mode: "known" | "tool_output" = "known",
|
|
1102
|
+
depth = 0,
|
|
1103
|
+
): E {
|
|
1104
|
+
// Tool output also masks secret-shaped parts (with or without known values).
|
|
1105
|
+
if (!memory || !(error instanceof Error) || depth > 4) return error;
|
|
1106
|
+
if (mode === "known" && memory.size === 0) return error;
|
|
1107
|
+
// Each part on its own, so one read-only field does not stop the rest.
|
|
1108
|
+
const field = (name: "message" | "stack") => {
|
|
1109
|
+
try {
|
|
1110
|
+
const text = error[name];
|
|
1111
|
+
if (typeof text !== "string") return;
|
|
1112
|
+
const masked =
|
|
1113
|
+
mode === "tool_output"
|
|
1114
|
+
? memory.maskToolOutputText(text, "tool_error")
|
|
1115
|
+
: memory.maskText(text, "tool_error");
|
|
1116
|
+
if (masked !== text) error[name] = masked;
|
|
1117
|
+
} catch {
|
|
1118
|
+
// The original text stays.
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
field("message");
|
|
1122
|
+
field("stack");
|
|
1123
|
+
try {
|
|
1124
|
+
if (error.cause !== undefined) maskSecretValuesInError(error.cause, memory, mode, depth + 1);
|
|
1125
|
+
} catch {
|
|
1126
|
+
// The cause stays as it is.
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
const inner = (error as { errors?: unknown }).errors;
|
|
1130
|
+
if (Array.isArray(inner)) {
|
|
1131
|
+
for (const item of inner.slice(0, 32)) {
|
|
1132
|
+
maskSecretValuesInError(item, memory, mode, depth + 1);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
} catch {
|
|
1136
|
+
// The inner errors stay as they are.
|
|
1137
|
+
}
|
|
1138
|
+
return error;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// ---------------------------------------------------------------------------
|
|
1142
|
+
// Explicit reveals
|
|
1143
|
+
// ---------------------------------------------------------------------------
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Opengeni's audited plaintext read, which hands a value to the agent on
|
|
1147
|
+
* purpose: exactly its own `secret_get` (as the agent sees it, bare or with
|
|
1148
|
+
* the first-party prefix). A connector's tool of the same name is not one.
|
|
1149
|
+
*/
|
|
1150
|
+
export function isSecretRevealToolName(name: string): boolean {
|
|
1151
|
+
return name === "secret_get" || name === "opengeni__secret_get";
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function decodedBase64Text(value: string): string | null {
|
|
1155
|
+
try {
|
|
1156
|
+
const binary = atob(value.replace(/\s+/gu, ""));
|
|
1157
|
+
const bytes = new Uint8Array(binary.length);
|
|
1158
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
1159
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1160
|
+
} catch {
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* The values a `secret_get` result reveals (`{ value, encoding }`, possibly
|
|
1167
|
+
* as JSON text). A base64 result reveals both the encoded text and the
|
|
1168
|
+
* decoded UTF-8 text, since the memory holds the decoded value.
|
|
1169
|
+
*/
|
|
1170
|
+
export function secretGetRevealedValues(result: unknown, depth = 0): string[] {
|
|
1171
|
+
if (depth > 6) return [];
|
|
1172
|
+
if (typeof result === "string") {
|
|
1173
|
+
const trimmed = result.trim();
|
|
1174
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return [];
|
|
1175
|
+
try {
|
|
1176
|
+
return secretGetRevealedValues(JSON.parse(trimmed), depth + 1);
|
|
1177
|
+
} catch {
|
|
1178
|
+
return [];
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (result === null || typeof result !== "object") return [];
|
|
1182
|
+
if (Array.isArray(result)) {
|
|
1183
|
+
return result.flatMap((item) => secretGetRevealedValues(item, depth + 1));
|
|
1184
|
+
}
|
|
1185
|
+
const record = result as Record<string, unknown>;
|
|
1186
|
+
if (typeof record.value === "string" && record.encoding === "utf8") return [record.value];
|
|
1187
|
+
if (typeof record.value === "string" && record.encoding === "base64") {
|
|
1188
|
+
const decoded = decodedBase64Text(record.value);
|
|
1189
|
+
return decoded === null ? [record.value] : [record.value, decoded];
|
|
1190
|
+
}
|
|
1191
|
+
return Object.values(record).flatMap((value) => secretGetRevealedValues(value, depth + 1));
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* When `toolName` is `secret_get`, mark what its result reveals as readable
|
|
1196
|
+
* for the rest of the memory's life, so masking the result leaves it intact.
|
|
1197
|
+
* Never throws.
|
|
1198
|
+
*/
|
|
1199
|
+
export function revealSecretGetResult(
|
|
1200
|
+
memory: SecretValueMemory | null | undefined,
|
|
1201
|
+
toolName: string,
|
|
1202
|
+
result: unknown,
|
|
1203
|
+
): void {
|
|
1204
|
+
if (!memory) return;
|
|
1205
|
+
try {
|
|
1206
|
+
if (!isSecretRevealToolName(toolName)) return;
|
|
1207
|
+
for (const value of secretGetRevealedValues(result)) memory.reveal(value);
|
|
1208
|
+
} catch {
|
|
1209
|
+
// Masking applies as usual.
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// ---------------------------------------------------------------------------
|
|
1214
|
+
// Resolver integration
|
|
1215
|
+
// ---------------------------------------------------------------------------
|
|
1216
|
+
|
|
1217
|
+
function textMaterial(value: ResolvedSecretValue): string | null {
|
|
1218
|
+
const material = value.material;
|
|
1219
|
+
if (material.type === "text") return material.value;
|
|
1220
|
+
if (material.type === "bytes") {
|
|
1221
|
+
try {
|
|
1222
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(material.value);
|
|
1223
|
+
} catch {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// A one-time code is six or eight digits that change every period: below
|
|
1228
|
+
// the useful minimum and not worth searching for.
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** Remember what one resolver call returned, labelled by reference; never throws. */
|
|
1233
|
+
export function rememberResolvedSecretValues(
|
|
1234
|
+
memory: SecretValueMemory | null | undefined,
|
|
1235
|
+
resolved: ReadonlyMap<string, ResolvedSecretValue>,
|
|
1236
|
+
operationId?: string,
|
|
1237
|
+
): void {
|
|
1238
|
+
if (!memory) return;
|
|
1239
|
+
for (const value of resolved.values()) {
|
|
1240
|
+
try {
|
|
1241
|
+
// A plain text field (a username, a host name) is not secret: masking it
|
|
1242
|
+
// would rewrite every page, log line and URL that mentions it.
|
|
1243
|
+
if (value.fieldKind === "text") continue;
|
|
1244
|
+
const text = textMaterial(value);
|
|
1245
|
+
if (text === null) continue;
|
|
1246
|
+
const label = secretReferenceMaskLabel({
|
|
1247
|
+
raw: value.reference.raw,
|
|
1248
|
+
itemId: value.itemId,
|
|
1249
|
+
fieldName: value.fieldName,
|
|
1250
|
+
});
|
|
1251
|
+
if (operationId) memory.rememberForOperation(operationId, text, label);
|
|
1252
|
+
else memory.remember(text, label);
|
|
1253
|
+
} catch {
|
|
1254
|
+
// Remembering never fails a resolution.
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** The same resolver, remembering every value it returns in the memory `target` names at resolution time. */
|
|
1260
|
+
export function rememberingSecretResolver(
|
|
1261
|
+
resolver: SecretResolver,
|
|
1262
|
+
target: SecretValueMemory | (() => SecretValueMemory | null | undefined),
|
|
1263
|
+
): SecretResolver {
|
|
1264
|
+
return {
|
|
1265
|
+
get principal() {
|
|
1266
|
+
return resolver.principal;
|
|
1267
|
+
},
|
|
1268
|
+
get enabled() {
|
|
1269
|
+
return resolver.enabled;
|
|
1270
|
+
},
|
|
1271
|
+
async resolve(refs, use) {
|
|
1272
|
+
const resolved = await resolver.resolve(refs, use);
|
|
1273
|
+
let memory: SecretValueMemory | null | undefined;
|
|
1274
|
+
try {
|
|
1275
|
+
memory = typeof target === "function" ? target() : target;
|
|
1276
|
+
} catch {
|
|
1277
|
+
memory = null;
|
|
1278
|
+
}
|
|
1279
|
+
rememberResolvedSecretValues(memory, resolved, use.operationId);
|
|
1280
|
+
return resolved;
|
|
1281
|
+
},
|
|
1282
|
+
};
|
|
1283
|
+
}
|