@superdurable/dex-agents 0.4.0 → 0.6.0

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.
Files changed (144) hide show
  1. package/README.md +616 -55
  2. package/dist/src/agent-approval-display.d.ts +135 -0
  3. package/dist/src/agent-approval-display.js +536 -0
  4. package/dist/src/agent-approval-display.js.map +1 -0
  5. package/dist/src/agent-context-compaction.d.ts +1 -1
  6. package/dist/src/agent-framework-runner.d.ts +160 -12
  7. package/dist/src/agent-framework-runner.js +4 -0
  8. package/dist/src/agent-framework-runner.js.map +1 -1
  9. package/dist/src/agent-local-directory-media-store.d.ts +111 -0
  10. package/dist/src/agent-local-directory-media-store.js +417 -0
  11. package/dist/src/agent-local-directory-media-store.js.map +1 -0
  12. package/dist/src/agent-media.d.ts +279 -0
  13. package/dist/src/agent-media.js +457 -0
  14. package/dist/src/agent-media.js.map +1 -0
  15. package/dist/src/agent-session-codecs.d.ts +5 -1
  16. package/dist/src/agent-session-codecs.js +76 -4
  17. package/dist/src/agent-session-codecs.js.map +1 -1
  18. package/dist/src/agent-session-failure.d.ts +4 -2
  19. package/dist/src/agent-session-failure.js +6 -2
  20. package/dist/src/agent-session-failure.js.map +1 -1
  21. package/dist/src/agent-session-flow-graph.d.ts +46 -0
  22. package/dist/src/agent-session-flow-graph.js +51 -0
  23. package/dist/src/agent-session-flow-graph.js.map +1 -1
  24. package/dist/src/agent-session-flow.d.ts +28 -3
  25. package/dist/src/agent-session-flow.js +21 -1
  26. package/dist/src/agent-session-flow.js.map +1 -1
  27. package/dist/src/agent-session-persistence.d.ts +33 -0
  28. package/dist/src/agent-session-persistence.js +22 -1
  29. package/dist/src/agent-session-persistence.js.map +1 -1
  30. package/dist/src/agent-session-start.d.ts +1 -1
  31. package/dist/src/agent-session-start.js +2 -2
  32. package/dist/src/agent-session-start.js.map +1 -1
  33. package/dist/src/agent-session-steps.d.ts +2 -0
  34. package/dist/src/agent-session-steps.js +74 -5
  35. package/dist/src/agent-session-steps.js.map +1 -1
  36. package/dist/src/agent-session-summary.d.ts +138 -0
  37. package/dist/src/agent-session-summary.js +151 -0
  38. package/dist/src/agent-session-summary.js.map +1 -0
  39. package/dist/src/agent-session-tracer.js +1 -1
  40. package/dist/src/agent-session-tracer.js.map +1 -1
  41. package/dist/src/agent-session-transitions.d.ts +11 -3
  42. package/dist/src/agent-session-transitions.js +58 -6
  43. package/dist/src/agent-session-transitions.js.map +1 -1
  44. package/dist/src/agent-session-values.d.ts +179 -2
  45. package/dist/src/agent-session-values.js.map +1 -1
  46. package/dist/src/agent-session-wait-steps.js +14 -2
  47. package/dist/src/agent-session-wait-steps.js.map +1 -1
  48. package/dist/src/ai-sdk/ai-sdk-model-client.d.ts +15 -3
  49. package/dist/src/ai-sdk/ai-sdk-model-client.js +36 -7
  50. package/dist/src/ai-sdk/ai-sdk-model-client.js.map +1 -1
  51. package/dist/src/ai-sdk/index.d.ts +1 -1
  52. package/dist/src/ai-sdk/index.js +1 -1
  53. package/dist/src/ai-sdk/index.js.map +1 -1
  54. package/dist/src/{stepwise → builtin-loop}/agent-definition.d.ts +120 -5
  55. package/dist/src/builtin-loop/agent-definition.js.map +1 -0
  56. package/dist/src/builtin-loop/builtin-agent-session-flow.d.ts +77 -0
  57. package/dist/src/{stepwise/stepwise-agent-session-flow.js → builtin-loop/builtin-agent-session-flow.js} +19 -9
  58. package/dist/src/builtin-loop/builtin-agent-session-flow.js.map +1 -0
  59. package/dist/src/{stepwise/stepwise-agent-runner.d.ts → builtin-loop/builtin-loop-agent-runner.d.ts} +72 -14
  60. package/dist/src/{stepwise/stepwise-agent-runner.js → builtin-loop/builtin-loop-agent-runner.js} +284 -41
  61. package/dist/src/builtin-loop/builtin-loop-agent-runner.js.map +1 -0
  62. package/dist/src/{stepwise/stepwise-input-items.d.ts → builtin-loop/builtin-loop-input-items.d.ts} +7 -3
  63. package/dist/src/{stepwise/stepwise-input-items.js → builtin-loop/builtin-loop-input-items.js} +57 -10
  64. package/dist/src/builtin-loop/builtin-loop-input-items.js.map +1 -0
  65. package/dist/src/builtin-loop/function-tool.d.ts +282 -0
  66. package/dist/src/{stepwise → builtin-loop}/function-tool.js +63 -3
  67. package/dist/src/builtin-loop/function-tool.js.map +1 -0
  68. package/dist/src/builtin-loop/index.d.ts +8 -0
  69. package/dist/src/builtin-loop/index.js +13 -0
  70. package/dist/src/builtin-loop/index.js.map +1 -0
  71. package/dist/src/builtin-loop/json-schema-validation.js.map +1 -0
  72. package/dist/src/{stepwise → builtin-loop}/model-client.d.ts +36 -7
  73. package/dist/src/builtin-loop/model-client.js.map +1 -0
  74. package/dist/src/{stepwise → builtin-loop}/scripted-model-client.d.ts +8 -5
  75. package/dist/src/{stepwise → builtin-loop}/scripted-model-client.js +8 -5
  76. package/dist/src/builtin-loop/scripted-model-client.js.map +1 -0
  77. package/dist/src/{stepwise → builtin-loop}/tool-source.d.ts +2 -2
  78. package/dist/src/builtin-loop/tool-source.js.map +1 -0
  79. package/dist/src/connectors/connector-input-schema.js +1 -1
  80. package/dist/src/connectors/connector-input-schema.js.map +1 -1
  81. package/dist/src/connectors/connector-step-context.d.ts +19 -2
  82. package/dist/src/connectors/connector-step-context.js +61 -5
  83. package/dist/src/connectors/connector-step-context.js.map +1 -1
  84. package/dist/src/connectors/connector-tool-source.d.ts +63 -14
  85. package/dist/src/connectors/connector-tool-source.js +124 -25
  86. package/dist/src/connectors/connector-tool-source.js.map +1 -1
  87. package/dist/src/index.d.ts +8 -4
  88. package/dist/src/index.js +5 -1
  89. package/dist/src/index.js.map +1 -1
  90. package/dist/src/mcp/index.d.ts +1 -0
  91. package/dist/src/mcp/index.js +1 -0
  92. package/dist/src/mcp/index.js.map +1 -1
  93. package/dist/src/mcp/mcp-dex-conventions.d.ts +57 -0
  94. package/dist/src/mcp/mcp-dex-conventions.js +90 -0
  95. package/dist/src/mcp/mcp-dex-conventions.js.map +1 -0
  96. package/dist/src/mcp/mcp-image-content.d.ts +20 -0
  97. package/dist/src/mcp/mcp-image-content.js +82 -0
  98. package/dist/src/mcp/mcp-image-content.js.map +1 -0
  99. package/dist/src/mcp/mcp-tool-source.d.ts +205 -18
  100. package/dist/src/mcp/mcp-tool-source.js +289 -47
  101. package/dist/src/mcp/mcp-tool-source.js.map +1 -1
  102. package/dist/src/s3/index.d.ts +1 -0
  103. package/dist/src/s3/index.js +9 -0
  104. package/dist/src/s3/index.js.map +1 -0
  105. package/dist/src/s3/s3-media-store.d.ts +138 -0
  106. package/dist/src/s3/s3-media-store.js +255 -0
  107. package/dist/src/s3/s3-media-store.js.map +1 -0
  108. package/dist/src/stepwise/index.d.ts +41 -8
  109. package/dist/src/stepwise/index.js +35 -5
  110. package/dist/src/stepwise/index.js.map +1 -1
  111. package/dist/src/testing/agent-session-test-client.d.ts +6 -3
  112. package/dist/src/testing/agent-session-test-client.js +10 -3
  113. package/dist/src/testing/agent-session-test-client.js.map +1 -1
  114. package/dist/src/testing/private-dex-engine.d.ts +8 -0
  115. package/dist/src/testing/private-dex-engine.js +17 -1
  116. package/dist/src/testing/private-dex-engine.js.map +1 -1
  117. package/dist/src/testing/scripted-model-client.d.ts +1 -1
  118. package/dist/src/testing/scripted-model-client.js +2 -2
  119. package/dist/src/testing/scripted-model-client.js.map +1 -1
  120. package/dist/src/tool-source-router.d.ts +3 -3
  121. package/dist/src/tool-source-router.js +2 -2
  122. package/dist/src/ui-stream/agent-event-json.d.ts +2 -1
  123. package/dist/src/ui-stream/agent-event-json.js +3 -1
  124. package/dist/src/ui-stream/agent-event-json.js.map +1 -1
  125. package/dist/src/ui-stream/agent-ui-messages.d.ts +11 -3
  126. package/dist/src/ui-stream/agent-ui-messages.js +29 -5
  127. package/dist/src/ui-stream/agent-ui-messages.js.map +1 -1
  128. package/package.json +17 -4
  129. package/dist/src/stepwise/agent-definition.js.map +0 -1
  130. package/dist/src/stepwise/function-tool.d.ts +0 -130
  131. package/dist/src/stepwise/function-tool.js.map +0 -1
  132. package/dist/src/stepwise/json-schema-validation.js.map +0 -1
  133. package/dist/src/stepwise/model-client.js.map +0 -1
  134. package/dist/src/stepwise/scripted-model-client.js.map +0 -1
  135. package/dist/src/stepwise/stepwise-agent-runner.js.map +0 -1
  136. package/dist/src/stepwise/stepwise-agent-session-flow.d.ts +0 -35
  137. package/dist/src/stepwise/stepwise-agent-session-flow.js.map +0 -1
  138. package/dist/src/stepwise/stepwise-input-items.js.map +0 -1
  139. package/dist/src/stepwise/tool-source.js.map +0 -1
  140. /package/dist/src/{stepwise → builtin-loop}/agent-definition.js +0 -0
  141. /package/dist/src/{stepwise → builtin-loop}/json-schema-validation.d.ts +0 -0
  142. /package/dist/src/{stepwise → builtin-loop}/json-schema-validation.js +0 -0
  143. /package/dist/src/{stepwise → builtin-loop}/model-client.js +0 -0
  144. /package/dist/src/{stepwise → builtin-loop}/tool-source.js +0 -0
@@ -0,0 +1,135 @@
1
+ import type { AgentInterruptDisplay } from "./agent-session-values.js";
2
+ /**
3
+ * Largest {@link AgentInterruptDisplay.preview} the session stores, in UTF-8 bytes: 16 KiB. The session drops a
4
+ * longer preview and keeps the target and summary, because the display is stored on the call's record, on its
5
+ * interrupt, and in its `interrupt_raised` event.
6
+ * @since 0.6.0
7
+ */
8
+ export declare const maxApprovalPreviewBytes = 16384;
9
+ /**
10
+ * Largest {@link AgentInterruptDisplay.target} the session stores, in UTF-8 bytes: 1 KiB. The session cuts a longer
11
+ * target and ends it with `…`.
12
+ * @since 0.6.0
13
+ */
14
+ export declare const maxApprovalTargetBytes = 1024;
15
+ /**
16
+ * Largest {@link AgentInterruptDisplay.summary} the session stores, in UTF-8 bytes: 2 KiB. The session cuts a longer
17
+ * summary and ends it with `…`.
18
+ * @since 0.6.0
19
+ */
20
+ export declare const maxApprovalSummaryBytes = 2048;
21
+ /**
22
+ * The text an {@link AgentInterruptDisplay} shows instead of a credential.
23
+ * @since 0.6.0
24
+ */
25
+ export declare const redactedArgumentValue = "<redacted>";
26
+ /**
27
+ * Whether an argument, header, field, or variable name looks like it holds a credential, so a display shows its
28
+ * value as {@link redactedArgumentValue}.
29
+ *
30
+ * The name is split into words at `_`, `-`, `.`, and spaces, and again at camelCase boundaries, ignoring case, so
31
+ * `pAsSwOrD` is one word. It is a credential
32
+ * name when a word is one of `apikey`, `auth`, `authorization`, `bearer`, `cookie`, `cred`, `credential`, `dsn`,
33
+ * `jwt`, `key`, `otp`, `pass`, `passphrase`, `passwd`, `password`, `privatekey`, `pwd`, `secret`, `session`,
34
+ * `sessionid`, `signature`, or `token` (and the plural forms `apikeys`, `cookies`, `creds`, `credentials`,
35
+ * `passwords`, `secrets`, and `tokens`), when a word ends in `apikey`, `credential`, `credentials`, `passwd`,
36
+ * `password`, `secret`, `secrets`, `token`, or `tokens`, as in `PGPASSWORD`, or when it holds the words
37
+ * `connection string` or `conn str`. So
38
+ * `X-Api-Key`, `clientSecret`, `private_key`, `credentials`, and `DATABASE_CONNECTION_STRING` all match, and
39
+ * `channel`, `monkey`, `tokenizer`, and `author` do not. The Python core applies the same rule; the shared cases
40
+ * are in `agents/conformance/approval_display.json`.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * isCredentialLikeName("clientSecret"); // true
45
+ * isCredentialLikeName("channel"); // false
46
+ * ```
47
+ * @param name - The name to check.
48
+ * @returns `true` when the name looks like it holds a credential.
49
+ * @since 0.6.0
50
+ */
51
+ export declare function isCredentialLikeName(name: string): boolean;
52
+ /**
53
+ * Replaces common credential shapes written inline in a text, such as a shell command or a file, with
54
+ * {@link redactedArgumentValue}, and leaves every other character as it is.
55
+ *
56
+ * It is best-effort hygiene that keeps common credentials out of an approval display, not a security boundary: a
57
+ * credential in a form it does not know, such as one spelled with Unicode lookalike letters, stays as it is, and the
58
+ * call's arguments still hold the full text.
59
+ *
60
+ * It reads `\uXXXX`, `\xXX`, and `%XX` escapes as the characters they stand for, so an escaped name such as
61
+ * `pass\u0077ord` or `%70assword` is found, but it returns the text with the escapes as written: only the characters
62
+ * of each credential found are replaced. It replaces private key blocks; the password of a URL such as
63
+ * `https://user:pass@host`; the password of `-u user:pass`, `--user`, `--proxy-user`, and `sshpass -p`; and the value
64
+ * after a name that {@link isCredentialLikeName} matches, in a `name=value` assignment, a `name: value` header or
65
+ * field, a JSON member, or a `--name value` flag, which covers query parameters such as `?token=…`. A value runs as a
66
+ * shell word: backslash escapes, line continuations, and adjacent quoted parts join it, escaped quotes stay inside a
67
+ * quoted part, and it ends at whitespace, `&`, `;`, `,`, `}`, `]`, or `)`; an unclosed quote runs to the end of its
68
+ * line. A triple-quoted value runs to its closing quotes, or to the end of the text, and a YAML block scalar after
69
+ * `name: |` or `name: >` covers its indented lines. The value of an `Authorization` or `Cookie` header runs to the end
70
+ * of its line, with folded and continued lines, and its scheme, such as `Bearer`, stays. A value that is exactly
71
+ * {@link redactedArgumentValue} stays. Outside a header, a token-shaped word after `Bearer`, `Basic`, or `Digest` is
72
+ * replaced, and so are common token shapes, such as AWS access key IDs and GitHub, Slack, GitLab, and OpenAI-style
73
+ * keys and JWTs. The rules are conservative: a generic `key=value` is replaced too. The Python core applies the same
74
+ * rules; the shared cases are in `agents/conformance/approval_display.json`.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * redactInlineCredentials('curl -H "Authorization: Bearer t1" https://api.example.com?token=t1');
79
+ * // 'curl -H "Authorization: Bearer <redacted>" https://api.example.com?token=<redacted>'
80
+ * redactInlineCredentials("printf '\\x41'"); // "printf '\\x41'", unchanged
81
+ * ```
82
+ * @param text - The text.
83
+ * @returns The text with each credential found replaced and nothing else changed.
84
+ * @since 0.6.0
85
+ */
86
+ export declare function redactInlineCredentials(text: string): string;
87
+ /**
88
+ * The connector, connection, operation, and arguments of one connector tool call, for
89
+ * {@link buildConnectorToolApprovalDisplay}.
90
+ * @since 0.6.0
91
+ */
92
+ export interface ConnectorToolApprovalDisplayInput {
93
+ /** The Connector ID, such as `slack`. */
94
+ readonly connectorId: string;
95
+ /** The connection's name, such as `support-slack`. */
96
+ readonly connectionName: string;
97
+ /** The operation the call runs, such as `postChannelMessage`. */
98
+ readonly operation: string;
99
+ /** The call's arguments as JSON text. */
100
+ readonly arguments: string;
101
+ }
102
+ /**
103
+ * Builds the approval display of one connector tool call from its operation, its connection, and its short
104
+ * arguments.
105
+ *
106
+ * The target is `<connectorId>/<connectionName>`. The summary names the operation and the connection and lists up to
107
+ * five top-level arguments in argument order: each string of at most 80 characters on one line, number, and boolean,
108
+ * as JSON. A credential-like argument (see {@link isCredentialLikeName}) shows {@link redactedArgumentValue} instead
109
+ * of its value, and string values and the connector, connection, and operation names have their inline credentials
110
+ * replaced (see {@link redactInlineCredentials}), as best-effort hygiene. Longer
111
+ * text, objects, and lists are left out; the approval UI shows them from the arguments. The summary never uses the
112
+ * operation's description, which says what the operation is for, not what this call does. `ConnectorToolSource` and
113
+ * the CLI's connector tools use it; a source of other connector tools may too.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * const display = buildConnectorToolApprovalDisplay({
118
+ * connectorId: "slack",
119
+ * connectionName: "support-slack",
120
+ * operation: "postChannelMessage",
121
+ * arguments: '{"channel":"#eng","text":"Deploy is done"}',
122
+ * });
123
+ * // display.summary: 'Calls slack postChannelMessage with connection support-slack; channel: "#eng", text: "Deploy is done".'
124
+ * ```
125
+ * @param input - The call's connector, connection, operation, and arguments.
126
+ * @returns The display, with no preview.
127
+ * @since 0.6.0
128
+ */
129
+ export declare function buildConnectorToolApprovalDisplay(input: ConnectorToolApprovalDisplayInput): AgentInterruptDisplay;
130
+ /**
131
+ * Returns the display the session stores for a call: `null` for none or one that is not a display; otherwise the
132
+ * target and summary cut to {@link maxApprovalTargetBytes} and {@link maxApprovalSummaryBytes}, and the preview, or
133
+ * `null` when it is over {@link maxApprovalPreviewBytes}.
134
+ */
135
+ export declare function boundApprovalDisplay(display: unknown): AgentInterruptDisplay | null;
@@ -0,0 +1,536 @@
1
+ // Copyright (c) 2026 Super Durable, Inc.
2
+ //
3
+ // Licensed under the Sustainable Use License 1.0.
4
+ // You may not use this file except in compliance with the License.
5
+ // See the LICENSE file in the repository root.
6
+ //
7
+ // SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+ /**
9
+ * Largest {@link AgentInterruptDisplay.preview} the session stores, in UTF-8 bytes: 16 KiB. The session drops a
10
+ * longer preview and keeps the target and summary, because the display is stored on the call's record, on its
11
+ * interrupt, and in its `interrupt_raised` event.
12
+ * @since 0.6.0
13
+ */
14
+ export const maxApprovalPreviewBytes = 16_384;
15
+ /**
16
+ * Largest {@link AgentInterruptDisplay.target} the session stores, in UTF-8 bytes: 1 KiB. The session cuts a longer
17
+ * target and ends it with `…`.
18
+ * @since 0.6.0
19
+ */
20
+ export const maxApprovalTargetBytes = 1_024;
21
+ /**
22
+ * Largest {@link AgentInterruptDisplay.summary} the session stores, in UTF-8 bytes: 2 KiB. The session cuts a longer
23
+ * summary and ends it with `…`.
24
+ * @since 0.6.0
25
+ */
26
+ export const maxApprovalSummaryBytes = 2_048;
27
+ /**
28
+ * The text an {@link AgentInterruptDisplay} shows instead of a credential.
29
+ * @since 0.6.0
30
+ */
31
+ export const redactedArgumentValue = "<redacted>";
32
+ const truncationMark = "…";
33
+ const credentialWords = new Set([
34
+ "apikey",
35
+ "apikeys",
36
+ "auth",
37
+ "authorization",
38
+ "bearer",
39
+ "cookie",
40
+ "cookies",
41
+ "cred",
42
+ "creds",
43
+ "credential",
44
+ "credentials",
45
+ "dsn",
46
+ "jwt",
47
+ "key",
48
+ "otp",
49
+ "pass",
50
+ "passphrase",
51
+ "passwd",
52
+ "password",
53
+ "passwords",
54
+ "privatekey",
55
+ "pwd",
56
+ "secret",
57
+ "secrets",
58
+ "session",
59
+ "sessionid",
60
+ "signature",
61
+ "token",
62
+ "tokens",
63
+ ]);
64
+ const credentialWordEndings = [
65
+ "apikey",
66
+ "credential",
67
+ "credentials",
68
+ "passwd",
69
+ "password",
70
+ "secret",
71
+ "secrets",
72
+ "token",
73
+ "tokens",
74
+ ];
75
+ const credentialPhrases = [
76
+ ["connection", "string"],
77
+ ["conn", "str"],
78
+ ];
79
+ const maxDisplayedArguments = 5;
80
+ const maxDisplayedArgumentCharacters = 80;
81
+ // Explicit ASCII classes keep the patterns identical to the Python core's, which compiles them with re.ASCII.
82
+ const escapeSequencePattern = /\\u([0-9a-fA-F]{4})|\\x([0-9a-fA-F]{2})|%([0-9a-fA-F]{2})/y;
83
+ const privateKeyBlockPattern = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/dg;
84
+ const urlUserInfoPattern = /([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^ \t\n\r\f\v/:@'"]+):([^ \t\n\r\f\v/@'"]+)@/dg;
85
+ const userFlagPattern = /(^|[ \t\n\r\f\v])(-u|--user|--proxy-user)([ \t]+|=)/g;
86
+ const sshpassPattern = /(^|[ \t\n\r\f\v])(sshpass[ \t]+-p)([ \t]*)/g;
87
+ const yamlBlockIndicatorPattern = /^[|>][+-]?[0-9]?[ \t]*(?:#[^\n]*)?$/;
88
+ const namedValuePattern = /(["']?)([A-Za-z_][A-Za-z0-9_.-]*)(["']?)([ \t]*)([:=])([ \t]*)/g;
89
+ const flagValuePattern = /(^|[ \t\n\r\f\v])(--?)([A-Za-z][A-Za-z0-9_-]*)([ \t]+)/g;
90
+ const standaloneSchemePattern = /\b(bearer|basic|digest)([ \t]+)([A-Za-z0-9._~+/-]+=*)/dgi;
91
+ const tokenShapedPattern = /[0-9+/=._~-]/;
92
+ const authorizationSchemePattern = /^(bearer|basic|digest|token|negotiate)[ \t]+/i;
93
+ const tokenShapesPattern = /\b(?:(?:AKIA|ASIA)[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abprs]-[A-Za-z0-9-]{10,}|sk-[A-Za-z0-9_-]{20,}|glpat-[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})/dg;
94
+ const bareValueStops = new Set([" ", "\t", "\n", "\r", "\f", "\v", "&", ";", ",", "}", "]", ")"]);
95
+ /**
96
+ * Whether an argument, header, field, or variable name looks like it holds a credential, so a display shows its
97
+ * value as {@link redactedArgumentValue}.
98
+ *
99
+ * The name is split into words at `_`, `-`, `.`, and spaces, and again at camelCase boundaries, ignoring case, so
100
+ * `pAsSwOrD` is one word. It is a credential
101
+ * name when a word is one of `apikey`, `auth`, `authorization`, `bearer`, `cookie`, `cred`, `credential`, `dsn`,
102
+ * `jwt`, `key`, `otp`, `pass`, `passphrase`, `passwd`, `password`, `privatekey`, `pwd`, `secret`, `session`,
103
+ * `sessionid`, `signature`, or `token` (and the plural forms `apikeys`, `cookies`, `creds`, `credentials`,
104
+ * `passwords`, `secrets`, and `tokens`), when a word ends in `apikey`, `credential`, `credentials`, `passwd`,
105
+ * `password`, `secret`, `secrets`, `token`, or `tokens`, as in `PGPASSWORD`, or when it holds the words
106
+ * `connection string` or `conn str`. So
107
+ * `X-Api-Key`, `clientSecret`, `private_key`, `credentials`, and `DATABASE_CONNECTION_STRING` all match, and
108
+ * `channel`, `monkey`, `tokenizer`, and `author` do not. The Python core applies the same rule; the shared cases
109
+ * are in `agents/conformance/approval_display.json`.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * isCredentialLikeName("clientSecret"); // true
114
+ * isCredentialLikeName("channel"); // false
115
+ * ```
116
+ * @param name - The name to check.
117
+ * @returns `true` when the name looks like it holds a credential.
118
+ * @since 0.6.0
119
+ */
120
+ export function isCredentialLikeName(name) {
121
+ // Whole lowercased words first, so `pAsSwOrD` matches before camelCase splits it.
122
+ return [name, name.replace(/(?<=[a-z0-9])(?=[A-Z])/g, "_")].some((spelling) => {
123
+ const words = spelling
124
+ .toLowerCase()
125
+ .split(/[^a-z0-9]+/)
126
+ .filter((word) => word !== "");
127
+ return (words.some((word) => credentialWords.has(word) || credentialWordEndings.some((ending) => word.endsWith(ending))) ||
128
+ credentialPhrases.some((phrase) => words.some((_word, index) => phrase.every((part, offset) => words[index + offset] === part))));
129
+ });
130
+ }
131
+ /**
132
+ * Replaces common credential shapes written inline in a text, such as a shell command or a file, with
133
+ * {@link redactedArgumentValue}, and leaves every other character as it is.
134
+ *
135
+ * It is best-effort hygiene that keeps common credentials out of an approval display, not a security boundary: a
136
+ * credential in a form it does not know, such as one spelled with Unicode lookalike letters, stays as it is, and the
137
+ * call's arguments still hold the full text.
138
+ *
139
+ * It reads `\uXXXX`, `\xXX`, and `%XX` escapes as the characters they stand for, so an escaped name such as
140
+ * `pass\u0077ord` or `%70assword` is found, but it returns the text with the escapes as written: only the characters
141
+ * of each credential found are replaced. It replaces private key blocks; the password of a URL such as
142
+ * `https://user:pass@host`; the password of `-u user:pass`, `--user`, `--proxy-user`, and `sshpass -p`; and the value
143
+ * after a name that {@link isCredentialLikeName} matches, in a `name=value` assignment, a `name: value` header or
144
+ * field, a JSON member, or a `--name value` flag, which covers query parameters such as `?token=…`. A value runs as a
145
+ * shell word: backslash escapes, line continuations, and adjacent quoted parts join it, escaped quotes stay inside a
146
+ * quoted part, and it ends at whitespace, `&`, `;`, `,`, `}`, `]`, or `)`; an unclosed quote runs to the end of its
147
+ * line. A triple-quoted value runs to its closing quotes, or to the end of the text, and a YAML block scalar after
148
+ * `name: |` or `name: >` covers its indented lines. The value of an `Authorization` or `Cookie` header runs to the end
149
+ * of its line, with folded and continued lines, and its scheme, such as `Bearer`, stays. A value that is exactly
150
+ * {@link redactedArgumentValue} stays. Outside a header, a token-shaped word after `Bearer`, `Basic`, or `Digest` is
151
+ * replaced, and so are common token shapes, such as AWS access key IDs and GitHub, Slack, GitLab, and OpenAI-style
152
+ * keys and JWTs. The rules are conservative: a generic `key=value` is replaced too. The Python core applies the same
153
+ * rules; the shared cases are in `agents/conformance/approval_display.json`.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * redactInlineCredentials('curl -H "Authorization: Bearer t1" https://api.example.com?token=t1');
158
+ * // 'curl -H "Authorization: Bearer <redacted>" https://api.example.com?token=<redacted>'
159
+ * redactInlineCredentials("printf '\\x41'"); // "printf '\\x41'", unchanged
160
+ * ```
161
+ * @param text - The text.
162
+ * @returns The text with each credential found replaced and nothing else changed.
163
+ * @since 0.6.0
164
+ */
165
+ export function redactInlineCredentials(text) {
166
+ let units = readTextUnits(text);
167
+ for (const findSecrets of secretFinders) {
168
+ const secrets = findSecrets(units.map((unit) => unit.text).join(""));
169
+ if (secrets.length > 0) {
170
+ units = replaceWithMarkers(units, secrets);
171
+ }
172
+ }
173
+ return units.map((unit) => (unit.isMarker ? redactedArgumentValue : text.slice(unit.start, unit.end))).join("");
174
+ }
175
+ const secretFinders = [
176
+ (text) => matchSpans(text, privateKeyBlockPattern, 0),
177
+ (text) => matchSpans(text, urlUserInfoPattern, 3),
178
+ (text) => findValueSecrets(text, userFlagPattern, () => "user"),
179
+ (text) => findValueSecrets(text, sshpassPattern, (_match, valueStart) => (text[valueStart] === "-" ? null : "value")),
180
+ (text) => findValueSecrets(text, namedValuePattern, (match, valueStart) => {
181
+ const isComparison = match[5] === "=" && text[valueStart] === "=";
182
+ if (isComparison || !isCredentialLikeName(match[2])) {
183
+ return null;
184
+ }
185
+ return isHeaderName(match[2]) ? "header" : "value";
186
+ }),
187
+ (text) => findValueSecrets(text, flagValuePattern, (match, valueStart) => isCredentialLikeName(match[3]) && text[valueStart] !== "-" ? "value" : null),
188
+ (text) => [...text.matchAll(standaloneSchemePattern)]
189
+ .filter((match) => tokenShapedPattern.test(match[3]))
190
+ .map((match) => [match.indices[3][0], match.indices[3][1]]),
191
+ (text) => matchSpans(text, tokenShapesPattern, 0),
192
+ ];
193
+ function readTextUnits(text) {
194
+ const units = [];
195
+ let index = 0;
196
+ while (index < text.length) {
197
+ escapeSequencePattern.lastIndex = index;
198
+ const escape = escapeSequencePattern.exec(text);
199
+ if (escape !== null && escape.index === index) {
200
+ const code = Number.parseInt(escape[1] ?? escape[2] ?? escape[3], 16);
201
+ // A lone surrogate reads as written, so the text the rules read stays valid Unicode.
202
+ if (code < 0xd800 || code > 0xdfff) {
203
+ units.push({ text: String.fromCharCode(code), start: index, end: index + escape[0].length, isMarker: false });
204
+ index += escape[0].length;
205
+ continue;
206
+ }
207
+ }
208
+ units.push({ text: text[index], start: index, end: index + 1, isMarker: false });
209
+ index += 1;
210
+ }
211
+ return units;
212
+ }
213
+ /** Replaces the units each span touches with one marker unit that covers their original text. */
214
+ function replaceWithMarkers(units, secrets) {
215
+ const replaced = [];
216
+ let offset = 0;
217
+ let secretIndex = 0;
218
+ let marker = null;
219
+ for (const unit of units) {
220
+ const unitStart = offset;
221
+ const unitEnd = offset + unit.text.length;
222
+ offset = unitEnd;
223
+ while (secretIndex < secrets.length && secrets[secretIndex][1] <= unitStart) {
224
+ secretIndex += 1;
225
+ }
226
+ const secret = secrets[secretIndex];
227
+ const isSecret = secret !== undefined && secret[0] < unitEnd && secret[1] > unitStart;
228
+ if (isSecret) {
229
+ marker = marker === null ? { start: unit.start, end: unit.end } : { start: marker.start, end: unit.end };
230
+ if (secret[1] <= unitEnd) {
231
+ replaced.push({ text: redactedArgumentValue, start: marker.start, end: marker.end, isMarker: true });
232
+ marker = null;
233
+ secretIndex += 1;
234
+ }
235
+ continue;
236
+ }
237
+ if (marker !== null) {
238
+ replaced.push({ text: redactedArgumentValue, start: marker.start, end: marker.end, isMarker: true });
239
+ marker = null;
240
+ }
241
+ replaced.push(unit);
242
+ }
243
+ if (marker !== null) {
244
+ replaced.push({ text: redactedArgumentValue, start: marker.start, end: marker.end, isMarker: true });
245
+ }
246
+ return replaced;
247
+ }
248
+ /** The span of the given group of each match. */
249
+ function matchSpans(text, pattern, group) {
250
+ return [...text.matchAll(pattern)].map((match) => [match.indices[group][0], match.indices[group][1]]);
251
+ }
252
+ /** The credential span of the value after each match the kind function accepts, scanning left to right. */
253
+ function findValueSecrets(text, pattern, kindOf) {
254
+ const secrets = [];
255
+ let position = 0;
256
+ for (;;) {
257
+ pattern.lastIndex = position;
258
+ const match = pattern.exec(text);
259
+ if (match === null) {
260
+ return secrets;
261
+ }
262
+ const valueStart = match.index + match[0].length;
263
+ const kind = kindOf(match, valueStart);
264
+ if (kind === null) {
265
+ position = valueStart;
266
+ continue;
267
+ }
268
+ const isNamed = pattern === namedValuePattern;
269
+ // A quote right before a name, with none after it, is the quote the value ends at.
270
+ const outerQuote = isNamed && match[1] !== "" && match[3] === "" ? match[1] : "";
271
+ const value = readValue(text, valueStart, kind, outerQuote, isNamed && match[5] === ":" ? match.index : -1);
272
+ if (value.secret !== null) {
273
+ secrets.push(value.secret);
274
+ }
275
+ position = Math.max(value.end, valueStart);
276
+ }
277
+ }
278
+ /**
279
+ * Reads the value that starts at `start`: a triple-quoted string, a YAML block scalar after `name:` (when
280
+ * `fieldStart` is the field's index), a header line with its folded and continued lines, or a shell word with its
281
+ * escapes, adjacent quoted parts, and line continuations. An unclosed quote runs to the end of its line, and an
282
+ * unclosed triple quote to the end of the text.
283
+ * @returns Where the value ends and the span that holds the credential, or `null` for none.
284
+ */
285
+ function readValue(text, start, kind, outerQuote, fieldStart) {
286
+ for (const delimiter of ['"""', "'''"]) {
287
+ if (text.startsWith(delimiter, start)) {
288
+ const close = text.indexOf(delimiter, start + 3);
289
+ const end = close < 0 ? text.length : close + 3;
290
+ return { end, secret: secretSpan(text, start + 3, close < 0 ? text.length : close) };
291
+ }
292
+ }
293
+ const lineEnd = findLineEnd(text, start);
294
+ if (fieldStart >= 0 && yamlBlockIndicatorPattern.test(text.slice(start, lineEnd))) {
295
+ const blockEnd = findYamlBlockEnd(text, fieldStart, lineEnd);
296
+ return { end: blockEnd, secret: blockEnd > lineEnd ? secretSpan(text, lineEnd + 1, blockEnd) : null };
297
+ }
298
+ if (kind === "header" && !isQuote(text[start])) {
299
+ const end = findHeaderEnd(text, start, outerQuote);
300
+ const scheme = authorizationSchemePattern.exec(text.slice(start, end));
301
+ return { end, secret: secretSpan(text, start + (scheme === null ? 0 : scheme[0].length), end) };
302
+ }
303
+ const end = findShellWordEnd(text, start, outerQuote);
304
+ const quote = text[start];
305
+ const isQuoted = isQuote(quote) && end - start >= 2 && text[end - 1] === quote;
306
+ let secretStart = isQuoted ? start + 1 : start;
307
+ const secretEnd = isQuoted ? end - 1 : end;
308
+ if (kind === "user") {
309
+ const separator = text.slice(secretStart, secretEnd).indexOf(":");
310
+ secretStart = separator < 0 ? secretEnd : secretStart + separator + 1;
311
+ }
312
+ return { end, secret: secretSpan(text, secretStart, secretEnd) };
313
+ }
314
+ /** The span, or `null` when it is empty or already exactly the marker. */
315
+ function secretSpan(text, start, end) {
316
+ const secret = text.slice(start, end);
317
+ return secret === "" || secret === redactedArgumentValue ? null : [start, end];
318
+ }
319
+ function findLineEnd(text, start) {
320
+ const newline = text.indexOf("\n", start);
321
+ return newline < 0 ? text.length : newline;
322
+ }
323
+ /** The end of the YAML block whose indicator line ends at `indicatorEnd`: its lines indented past the field's. */
324
+ function findYamlBlockEnd(text, fieldStart, indicatorEnd) {
325
+ const fieldLineStart = text.lastIndexOf("\n", fieldStart - 1) + 1;
326
+ const fieldIndent = countIndent(text, fieldLineStart);
327
+ let blockEnd = indicatorEnd;
328
+ let lineStart = indicatorEnd + 1;
329
+ while (lineStart <= text.length && indicatorEnd < text.length) {
330
+ const lineEnd = findLineEnd(text, lineStart);
331
+ const isBlank = text.slice(lineStart, lineEnd).trim() === "";
332
+ if (!isBlank && countIndent(text, lineStart) <= fieldIndent) {
333
+ break;
334
+ }
335
+ if (!isBlank) {
336
+ blockEnd = lineEnd;
337
+ }
338
+ if (lineEnd >= text.length) {
339
+ break;
340
+ }
341
+ lineStart = lineEnd + 1;
342
+ }
343
+ return blockEnd;
344
+ }
345
+ function countIndent(text, lineStart) {
346
+ let index = lineStart;
347
+ while (text[index] === " " || text[index] === "\t") {
348
+ index += 1;
349
+ }
350
+ return index - lineStart;
351
+ }
352
+ /** The end of a header value: its line, and each next line a `\\` continues or that starts with a space or tab. */
353
+ function findHeaderEnd(text, start, outerQuote) {
354
+ let index = start;
355
+ while (index < text.length && text[index] !== outerQuote) {
356
+ if (text[index] === "\\" && (text[index + 1] === "\n" || text[index + 1] === "\r")) {
357
+ index += text[index + 1] === "\r" && text[index + 2] === "\n" ? 3 : 2;
358
+ continue;
359
+ }
360
+ if (text[index] === "\n" || text[index] === "\r") {
361
+ const next = text[index] === "\r" && text[index + 1] === "\n" ? index + 2 : index + 1;
362
+ if (text[next] === " " || text[next] === "\t") {
363
+ index = next;
364
+ continue;
365
+ }
366
+ break;
367
+ }
368
+ index += 1;
369
+ }
370
+ return trimEndIndex(text, start, index);
371
+ }
372
+ function trimEndIndex(text, start, end) {
373
+ let index = end;
374
+ while (index > start && (text[index - 1] === "\r" || text[index - 1] === "\n")) {
375
+ index -= 1;
376
+ }
377
+ return index;
378
+ }
379
+ /**
380
+ * The end of a shell word, or of a JSON or YAML value: a backslash escapes the next character, a line continuation
381
+ * included; a closed quoted part continues the word; an unclosed quote runs to the end of its line; and a stop
382
+ * character or `outerQuote` ends it.
383
+ */
384
+ function findShellWordEnd(text, start, outerQuote) {
385
+ let index = start;
386
+ while (index < text.length) {
387
+ const character = text[index];
388
+ if (outerQuote !== "" && character === outerQuote) {
389
+ break;
390
+ }
391
+ if (character === "\\") {
392
+ index += 2;
393
+ continue;
394
+ }
395
+ if (isQuote(character)) {
396
+ const quotedEnd = findQuotedEnd(text, index);
397
+ if (quotedEnd - 1 > index && text[quotedEnd - 1] === character) {
398
+ index = quotedEnd;
399
+ continue;
400
+ }
401
+ return findLineEnd(text, index);
402
+ }
403
+ if (bareValueStops.has(character)) {
404
+ break;
405
+ }
406
+ index += 1;
407
+ }
408
+ return Math.min(index, text.length);
409
+ }
410
+ /** The index after the quote that closes the one at `start`, skipping escaped double quotes, or the line end. */
411
+ function findQuotedEnd(text, start) {
412
+ const quote = text[start];
413
+ let index = start + 1;
414
+ while (index < text.length && text[index] !== quote && text[index] !== "\n") {
415
+ index += quote === '"' && text[index] === "\\" ? 2 : 1;
416
+ }
417
+ return Math.min(index < text.length && text[index] === quote ? index + 1 : index, text.length);
418
+ }
419
+ function isHeaderName(name) {
420
+ const words = name.toLowerCase().split(/[^a-z0-9]+/);
421
+ return words.includes("authorization") || words.includes("cookie") || words.includes("cookies");
422
+ }
423
+ function isQuote(character) {
424
+ return character === '"' || character === "'";
425
+ }
426
+ /**
427
+ * Builds the approval display of one connector tool call from its operation, its connection, and its short
428
+ * arguments.
429
+ *
430
+ * The target is `<connectorId>/<connectionName>`. The summary names the operation and the connection and lists up to
431
+ * five top-level arguments in argument order: each string of at most 80 characters on one line, number, and boolean,
432
+ * as JSON. A credential-like argument (see {@link isCredentialLikeName}) shows {@link redactedArgumentValue} instead
433
+ * of its value, and string values and the connector, connection, and operation names have their inline credentials
434
+ * replaced (see {@link redactInlineCredentials}), as best-effort hygiene. Longer
435
+ * text, objects, and lists are left out; the approval UI shows them from the arguments. The summary never uses the
436
+ * operation's description, which says what the operation is for, not what this call does. `ConnectorToolSource` and
437
+ * the CLI's connector tools use it; a source of other connector tools may too.
438
+ *
439
+ * @example
440
+ * ```ts
441
+ * const display = buildConnectorToolApprovalDisplay({
442
+ * connectorId: "slack",
443
+ * connectionName: "support-slack",
444
+ * operation: "postChannelMessage",
445
+ * arguments: '{"channel":"#eng","text":"Deploy is done"}',
446
+ * });
447
+ * // display.summary: 'Calls slack postChannelMessage with connection support-slack; channel: "#eng", text: "Deploy is done".'
448
+ * ```
449
+ * @param input - The call's connector, connection, operation, and arguments.
450
+ * @returns The display, with no preview.
451
+ * @since 0.6.0
452
+ */
453
+ export function buildConnectorToolApprovalDisplay(input) {
454
+ const keyArguments = describeKeyArguments(input.arguments);
455
+ const connectorId = redactInlineCredentials(input.connectorId);
456
+ const connectionName = redactInlineCredentials(input.connectionName);
457
+ const head = `Calls ${connectorId} ${redactInlineCredentials(input.operation)} with connection ${connectionName}`;
458
+ return {
459
+ target: `${connectorId}/${connectionName}`,
460
+ summary: keyArguments.length === 0 ? `${head}.` : `${head}; ${keyArguments.join(", ")}.`,
461
+ preview: null,
462
+ };
463
+ }
464
+ function describeKeyArguments(argumentsText) {
465
+ let parsed;
466
+ try {
467
+ parsed = JSON.parse(argumentsText);
468
+ }
469
+ catch {
470
+ return [];
471
+ }
472
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
473
+ return [];
474
+ }
475
+ const described = [];
476
+ for (const [name, value] of Object.entries(parsed)) {
477
+ if (described.length === maxDisplayedArguments) {
478
+ break;
479
+ }
480
+ if (isCredentialLikeName(name)) {
481
+ described.push(`${name}: ${redactedArgumentValue}`);
482
+ }
483
+ else if (isShortScalar(value)) {
484
+ described.push(`${name}: ${JSON.stringify(typeof value === "string" ? redactInlineCredentials(value) : value)}`);
485
+ }
486
+ }
487
+ return described;
488
+ }
489
+ function isShortScalar(value) {
490
+ if (typeof value === "string") {
491
+ return !/[\r\n]/.test(value) && Array.from(value).length <= maxDisplayedArgumentCharacters;
492
+ }
493
+ return typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value));
494
+ }
495
+ /**
496
+ * Returns the display the session stores for a call: `null` for none or one that is not a display; otherwise the
497
+ * target and summary cut to {@link maxApprovalTargetBytes} and {@link maxApprovalSummaryBytes}, and the preview, or
498
+ * `null` when it is over {@link maxApprovalPreviewBytes}.
499
+ */
500
+ export function boundApprovalDisplay(display) {
501
+ if (typeof display !== "object" || display === null) {
502
+ return null;
503
+ }
504
+ const { target, summary, preview } = display;
505
+ if (typeof target !== "string" || typeof summary !== "string") {
506
+ return null;
507
+ }
508
+ const isPreviewKept = typeof preview === "string" && utf8Length(preview) <= maxApprovalPreviewBytes;
509
+ return {
510
+ target: truncateUtf8(target, maxApprovalTargetBytes),
511
+ summary: truncateUtf8(summary, maxApprovalSummaryBytes),
512
+ preview: isPreviewKept ? preview : null,
513
+ };
514
+ }
515
+ /** The text, or its longest prefix of whole code points that fits `maxBytes` with `…` appended. */
516
+ function truncateUtf8(text, maxBytes) {
517
+ if (utf8Length(text) <= maxBytes) {
518
+ return text;
519
+ }
520
+ const budget = maxBytes - utf8Length(truncationMark);
521
+ let kept = "";
522
+ let used = 0;
523
+ for (const character of text) {
524
+ const size = utf8Length(character);
525
+ if (used + size > budget) {
526
+ break;
527
+ }
528
+ kept += character;
529
+ used += size;
530
+ }
531
+ return `${kept}${truncationMark}`;
532
+ }
533
+ function utf8Length(text) {
534
+ return new TextEncoder().encode(text).length;
535
+ }
536
+ //# sourceMappingURL=agent-approval-display.js.map