@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.
- package/README.md +616 -55
- package/dist/src/agent-approval-display.d.ts +135 -0
- package/dist/src/agent-approval-display.js +536 -0
- package/dist/src/agent-approval-display.js.map +1 -0
- package/dist/src/agent-context-compaction.d.ts +1 -1
- package/dist/src/agent-framework-runner.d.ts +160 -12
- package/dist/src/agent-framework-runner.js +4 -0
- package/dist/src/agent-framework-runner.js.map +1 -1
- package/dist/src/agent-local-directory-media-store.d.ts +111 -0
- package/dist/src/agent-local-directory-media-store.js +417 -0
- package/dist/src/agent-local-directory-media-store.js.map +1 -0
- package/dist/src/agent-media.d.ts +279 -0
- package/dist/src/agent-media.js +457 -0
- package/dist/src/agent-media.js.map +1 -0
- package/dist/src/agent-session-codecs.d.ts +5 -1
- package/dist/src/agent-session-codecs.js +76 -4
- package/dist/src/agent-session-codecs.js.map +1 -1
- package/dist/src/agent-session-failure.d.ts +4 -2
- package/dist/src/agent-session-failure.js +6 -2
- package/dist/src/agent-session-failure.js.map +1 -1
- package/dist/src/agent-session-flow-graph.d.ts +46 -0
- package/dist/src/agent-session-flow-graph.js +51 -0
- package/dist/src/agent-session-flow-graph.js.map +1 -1
- package/dist/src/agent-session-flow.d.ts +28 -3
- package/dist/src/agent-session-flow.js +21 -1
- package/dist/src/agent-session-flow.js.map +1 -1
- package/dist/src/agent-session-persistence.d.ts +33 -0
- package/dist/src/agent-session-persistence.js +22 -1
- package/dist/src/agent-session-persistence.js.map +1 -1
- package/dist/src/agent-session-start.d.ts +1 -1
- package/dist/src/agent-session-start.js +2 -2
- package/dist/src/agent-session-start.js.map +1 -1
- package/dist/src/agent-session-steps.d.ts +2 -0
- package/dist/src/agent-session-steps.js +74 -5
- package/dist/src/agent-session-steps.js.map +1 -1
- package/dist/src/agent-session-summary.d.ts +138 -0
- package/dist/src/agent-session-summary.js +151 -0
- package/dist/src/agent-session-summary.js.map +1 -0
- package/dist/src/agent-session-tracer.js +1 -1
- package/dist/src/agent-session-tracer.js.map +1 -1
- package/dist/src/agent-session-transitions.d.ts +11 -3
- package/dist/src/agent-session-transitions.js +58 -6
- package/dist/src/agent-session-transitions.js.map +1 -1
- package/dist/src/agent-session-values.d.ts +179 -2
- package/dist/src/agent-session-values.js.map +1 -1
- package/dist/src/agent-session-wait-steps.js +14 -2
- package/dist/src/agent-session-wait-steps.js.map +1 -1
- package/dist/src/ai-sdk/ai-sdk-model-client.d.ts +15 -3
- package/dist/src/ai-sdk/ai-sdk-model-client.js +36 -7
- package/dist/src/ai-sdk/ai-sdk-model-client.js.map +1 -1
- package/dist/src/ai-sdk/index.d.ts +1 -1
- package/dist/src/ai-sdk/index.js +1 -1
- package/dist/src/ai-sdk/index.js.map +1 -1
- package/dist/src/{stepwise → builtin-loop}/agent-definition.d.ts +120 -5
- package/dist/src/builtin-loop/agent-definition.js.map +1 -0
- package/dist/src/builtin-loop/builtin-agent-session-flow.d.ts +77 -0
- package/dist/src/{stepwise/stepwise-agent-session-flow.js → builtin-loop/builtin-agent-session-flow.js} +19 -9
- package/dist/src/builtin-loop/builtin-agent-session-flow.js.map +1 -0
- package/dist/src/{stepwise/stepwise-agent-runner.d.ts → builtin-loop/builtin-loop-agent-runner.d.ts} +72 -14
- package/dist/src/{stepwise/stepwise-agent-runner.js → builtin-loop/builtin-loop-agent-runner.js} +284 -41
- package/dist/src/builtin-loop/builtin-loop-agent-runner.js.map +1 -0
- package/dist/src/{stepwise/stepwise-input-items.d.ts → builtin-loop/builtin-loop-input-items.d.ts} +7 -3
- package/dist/src/{stepwise/stepwise-input-items.js → builtin-loop/builtin-loop-input-items.js} +57 -10
- package/dist/src/builtin-loop/builtin-loop-input-items.js.map +1 -0
- package/dist/src/builtin-loop/function-tool.d.ts +282 -0
- package/dist/src/{stepwise → builtin-loop}/function-tool.js +63 -3
- package/dist/src/builtin-loop/function-tool.js.map +1 -0
- package/dist/src/builtin-loop/index.d.ts +8 -0
- package/dist/src/builtin-loop/index.js +13 -0
- package/dist/src/builtin-loop/index.js.map +1 -0
- package/dist/src/builtin-loop/json-schema-validation.js.map +1 -0
- package/dist/src/{stepwise → builtin-loop}/model-client.d.ts +36 -7
- package/dist/src/builtin-loop/model-client.js.map +1 -0
- package/dist/src/{stepwise → builtin-loop}/scripted-model-client.d.ts +8 -5
- package/dist/src/{stepwise → builtin-loop}/scripted-model-client.js +8 -5
- package/dist/src/builtin-loop/scripted-model-client.js.map +1 -0
- package/dist/src/{stepwise → builtin-loop}/tool-source.d.ts +2 -2
- package/dist/src/builtin-loop/tool-source.js.map +1 -0
- package/dist/src/connectors/connector-input-schema.js +1 -1
- package/dist/src/connectors/connector-input-schema.js.map +1 -1
- package/dist/src/connectors/connector-step-context.d.ts +19 -2
- package/dist/src/connectors/connector-step-context.js +61 -5
- package/dist/src/connectors/connector-step-context.js.map +1 -1
- package/dist/src/connectors/connector-tool-source.d.ts +63 -14
- package/dist/src/connectors/connector-tool-source.js +124 -25
- package/dist/src/connectors/connector-tool-source.js.map +1 -1
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/index.d.ts +1 -0
- package/dist/src/mcp/index.js +1 -0
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/mcp/mcp-dex-conventions.d.ts +57 -0
- package/dist/src/mcp/mcp-dex-conventions.js +90 -0
- package/dist/src/mcp/mcp-dex-conventions.js.map +1 -0
- package/dist/src/mcp/mcp-image-content.d.ts +20 -0
- package/dist/src/mcp/mcp-image-content.js +82 -0
- package/dist/src/mcp/mcp-image-content.js.map +1 -0
- package/dist/src/mcp/mcp-tool-source.d.ts +205 -18
- package/dist/src/mcp/mcp-tool-source.js +289 -47
- package/dist/src/mcp/mcp-tool-source.js.map +1 -1
- package/dist/src/s3/index.d.ts +1 -0
- package/dist/src/s3/index.js +9 -0
- package/dist/src/s3/index.js.map +1 -0
- package/dist/src/s3/s3-media-store.d.ts +138 -0
- package/dist/src/s3/s3-media-store.js +255 -0
- package/dist/src/s3/s3-media-store.js.map +1 -0
- package/dist/src/stepwise/index.d.ts +41 -8
- package/dist/src/stepwise/index.js +35 -5
- package/dist/src/stepwise/index.js.map +1 -1
- package/dist/src/testing/agent-session-test-client.d.ts +6 -3
- package/dist/src/testing/agent-session-test-client.js +10 -3
- package/dist/src/testing/agent-session-test-client.js.map +1 -1
- package/dist/src/testing/private-dex-engine.d.ts +8 -0
- package/dist/src/testing/private-dex-engine.js +17 -1
- package/dist/src/testing/private-dex-engine.js.map +1 -1
- package/dist/src/testing/scripted-model-client.d.ts +1 -1
- package/dist/src/testing/scripted-model-client.js +2 -2
- package/dist/src/testing/scripted-model-client.js.map +1 -1
- package/dist/src/tool-source-router.d.ts +3 -3
- package/dist/src/tool-source-router.js +2 -2
- package/dist/src/ui-stream/agent-event-json.d.ts +2 -1
- package/dist/src/ui-stream/agent-event-json.js +3 -1
- package/dist/src/ui-stream/agent-event-json.js.map +1 -1
- package/dist/src/ui-stream/agent-ui-messages.d.ts +11 -3
- package/dist/src/ui-stream/agent-ui-messages.js +29 -5
- package/dist/src/ui-stream/agent-ui-messages.js.map +1 -1
- package/package.json +17 -4
- package/dist/src/stepwise/agent-definition.js.map +0 -1
- package/dist/src/stepwise/function-tool.d.ts +0 -130
- package/dist/src/stepwise/function-tool.js.map +0 -1
- package/dist/src/stepwise/json-schema-validation.js.map +0 -1
- package/dist/src/stepwise/model-client.js.map +0 -1
- package/dist/src/stepwise/scripted-model-client.js.map +0 -1
- package/dist/src/stepwise/stepwise-agent-runner.js.map +0 -1
- package/dist/src/stepwise/stepwise-agent-session-flow.d.ts +0 -35
- package/dist/src/stepwise/stepwise-agent-session-flow.js.map +0 -1
- package/dist/src/stepwise/stepwise-input-items.js.map +0 -1
- package/dist/src/stepwise/tool-source.js.map +0 -1
- /package/dist/src/{stepwise → builtin-loop}/agent-definition.js +0 -0
- /package/dist/src/{stepwise → builtin-loop}/json-schema-validation.d.ts +0 -0
- /package/dist/src/{stepwise → builtin-loop}/json-schema-validation.js +0 -0
- /package/dist/src/{stepwise → builtin-loop}/model-client.js +0 -0
- /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
|