testronaut 1.3.4 → 1.3.6

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.
@@ -3,6 +3,20 @@
3
3
 
4
4
  "image": "mcr.microsoft.com/devcontainers/javascript-node:22",
5
5
 
6
+ "features": {
7
+ "ghcr.io/devcontainers/features/github-cli:1": {}
8
+ },
9
+
10
+ "secrets": {
11
+ "GH_TOKEN": {
12
+ "description": "A GitHub PAT with read access to mission-testronaut/testronaut-demo."
13
+ }
14
+ },
15
+
16
+ "containerEnv": {
17
+ "GH_TOKEN": "${localEnv:GH_TOKEN}"
18
+ },
19
+
6
20
  "customizations": {
7
21
  "codespaces": {
8
22
  "repositories": {
@@ -11,6 +25,9 @@
11
25
  },
12
26
  "mission-testronaut/testronaut-examples": {
13
27
  "permissions": "write-all"
28
+ },
29
+ "mission-testronaut/testronaut-demo": {
30
+ "permissions": "write-all"
14
31
  }
15
32
  }
16
33
  },
@@ -29,16 +46,17 @@
29
46
  "eamodio.gitlens",
30
47
  "esbenp.prettier-vscode",
31
48
  "dbaeumer.vscode-eslint",
32
- "openai.chatgpt"
49
+ "openai.chatgpt",
50
+ "anthropic.claude-code"
33
51
  ]
34
52
  }
35
53
  },
36
54
 
37
55
  "workspaceFolder": "/workspaces",
38
56
 
39
- "postCreateCommand": "bash -lc 'set -e; cd /workspaces; if [ ! -d \"testronaut-examples/.git\" ]; then git clone https://github.com/mission-testronaut/testronaut-examples.git; fi'",
57
+ "postCreateCommand": "bash /workspaces/testronaut-cli/.devcontainer/setup-github-auth.sh",
40
58
 
41
- "postStartCommand": "bash -lc 'git config --global pull.rebase false && git config --global init.defaultBranch main'",
59
+ "postStartCommand": "bash /workspaces/testronaut-cli/.devcontainer/setup-github-auth.sh",
42
60
 
43
61
  "hostRequirements": {
44
62
  "cpus": 4,
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cd /workspaces
5
+
6
+ git config --global pull.rebase false
7
+ git config --global init.defaultBranch main
8
+
9
+ if ! command -v gh >/dev/null 2>&1; then
10
+ echo "GitHub CLI is not installed. Rebuild the devcontainer so the github-cli feature is installed."
11
+ exit 1
12
+ fi
13
+
14
+ if [ -n "${GH_TOKEN:-}" ] && ! gh auth status --hostname github.com >/dev/null 2>&1; then
15
+ printf '%s\n' "$GH_TOKEN" | gh auth login --hostname github.com --with-token >/dev/null
16
+ fi
17
+
18
+ if gh auth status --hostname github.com >/dev/null 2>&1; then
19
+ gh auth setup-git --hostname github.com
20
+ else
21
+ echo "GitHub CLI is not authenticated."
22
+ echo "Add GH_TOKEN as a Codespaces secret with access to mission-testronaut/testronaut-demo."
23
+ exit 1
24
+ fi
25
+
26
+ clone_if_missing() {
27
+ local repo="$1"
28
+ local directory="$2"
29
+
30
+ if [ ! -d "$directory/.git" ]; then
31
+ gh repo clone "$repo" "$directory"
32
+ fi
33
+ }
34
+
35
+ clone_if_missing mission-testronaut/testronaut-examples testronaut-examples
36
+ clone_if_missing mission-testronaut/testronaut-demo testronaut-demo
@@ -0,0 +1,168 @@
1
+ name: Prepare Package Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ release_type:
7
+ description: Semver increment to prepare
8
+ required: true
9
+ type: choice
10
+ default: patch
11
+ options:
12
+ - patch
13
+ - minor
14
+ - major
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ concurrency:
20
+ group: prepare-package-release
21
+ cancel-in-progress: false
22
+
23
+ jobs:
24
+ authorize-release:
25
+ name: Authorize Release
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - name: Require main branch
29
+ run: |
30
+ if [ "${GITHUB_REF_NAME}" != "main" ]; then
31
+ echo "Manual release preparation is only allowed from main."
32
+ exit 1
33
+ fi
34
+
35
+ - name: Require repository admin permission
36
+ uses: actions/github-script@v7
37
+ with:
38
+ script: |
39
+ const owner = context.repo.owner;
40
+ const repo = context.repo.repo;
41
+ const actor = context.actor;
42
+
43
+ if (owner.toLowerCase() === actor.toLowerCase()) {
44
+ core.info(`Actor ${actor} matches the repository owner and is treated as admin.`);
45
+ return;
46
+ }
47
+
48
+ const response = await github.rest.repos.getCollaboratorPermissionLevel({
49
+ owner,
50
+ repo,
51
+ username: actor,
52
+ });
53
+
54
+ const { permission, role_name: roleName } = response.data;
55
+ core.info(`Actor ${actor} permission=${permission}, role_name=${roleName}`);
56
+
57
+ if (permission !== "admin" && roleName !== "admin") {
58
+ core.setFailed(`Only repository admins can prepare package releases. ${actor} has ${roleName || permission}.`);
59
+ }
60
+
61
+ prepare-release:
62
+ name: Prepare Release Pull Request
63
+ needs: authorize-release
64
+ runs-on: ubuntu-latest
65
+ permissions:
66
+ # Required because this workflow pushes a release branch and opens a PR.
67
+ contents: write
68
+ pull-requests: write
69
+ steps:
70
+ - name: Checkout
71
+ uses: actions/checkout@v4
72
+ with:
73
+ fetch-depth: 0
74
+
75
+ - name: Setup Node
76
+ uses: actions/setup-node@v4
77
+ with:
78
+ node-version: 24
79
+ cache: npm
80
+
81
+ - name: Install dependencies
82
+ run: npm ci
83
+
84
+ - name: Run automated test suite
85
+ run: npm test
86
+
87
+ - name: Bump package version
88
+ run: npm version "${{ inputs.release_type }}" --no-git-tag-version
89
+
90
+ - name: Read new version
91
+ id: version
92
+ run: |
93
+ VERSION="$(node -e 'process.stdout.write(require("./package.json").version)')"
94
+ echo "value=${VERSION}" >> "$GITHUB_OUTPUT"
95
+ echo "branch=release/v${VERSION}" >> "$GITHUB_OUTPUT"
96
+
97
+ - name: Ensure release branch is not already open
98
+ env:
99
+ RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
100
+ run: |
101
+ if git ls-remote --exit-code --heads origin "${RELEASE_BRANCH}" >/dev/null 2>&1; then
102
+ echo "Release branch ${RELEASE_BRANCH} already exists on origin."
103
+ exit 1
104
+ fi
105
+
106
+ - name: Create release branch
107
+ env:
108
+ RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
109
+ run: git switch -c "${RELEASE_BRANCH}"
110
+
111
+ - name: Configure git author
112
+ run: |
113
+ git config user.name "github-actions[bot]"
114
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
115
+
116
+ - name: Commit release version
117
+ env:
118
+ VERSION: ${{ steps.version.outputs.value }}
119
+ run: |
120
+ git add package.json package-lock.json
121
+ git commit -m "chore(release): v${VERSION}"
122
+
123
+ - name: Push release branch
124
+ env:
125
+ RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
126
+ run: git push --set-upstream origin "${RELEASE_BRANCH}"
127
+
128
+ - name: Open release pull request
129
+ id: pull-request
130
+ uses: actions/github-script@v7
131
+ env:
132
+ RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
133
+ VERSION: ${{ steps.version.outputs.value }}
134
+ with:
135
+ script: |
136
+ const owner = context.repo.owner;
137
+ const repo = context.repo.repo;
138
+ const head = process.env.RELEASE_BRANCH;
139
+ const version = process.env.VERSION;
140
+
141
+ const pr = await github.rest.pulls.create({
142
+ owner,
143
+ repo,
144
+ title: `chore(release): v${version}`,
145
+ head,
146
+ base: "main",
147
+ body: [
148
+ `Prepares \`v${version}\` for npm publication.`,
149
+ "",
150
+ "After this pull request merges into `main`, the publish workflow will release the package to npm.",
151
+ ].join("\n"),
152
+ });
153
+
154
+ core.setOutput("url", pr.data.html_url);
155
+
156
+ - name: Write job summary
157
+ env:
158
+ VERSION: ${{ steps.version.outputs.value }}
159
+ RELEASE_BRANCH: ${{ steps.version.outputs.branch }}
160
+ PR_URL: ${{ steps.pull-request.outputs.url }}
161
+ run: |
162
+ {
163
+ echo "## Release PR created"
164
+ echo
165
+ echo "- Version: \`${VERSION}\`"
166
+ echo "- Release branch: \`${RELEASE_BRANCH}\`"
167
+ echo "- Pull request: ${PR_URL}"
168
+ } >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,133 @@
1
+ name: Publish Package
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - package.json
9
+ - package-lock.json
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: write
14
+ id-token: write
15
+
16
+ concurrency:
17
+ group: publish-package
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ publish-package:
22
+ name: Publish Package
23
+ runs-on: ubuntu-latest
24
+ environment:
25
+ # Use the same environment name in npm trusted publisher settings if you
26
+ # want npm to bind publishing to this protected environment.
27
+ name: package-publish
28
+ steps:
29
+ - name: Checkout
30
+ uses: actions/checkout@v4
31
+ with:
32
+ fetch-depth: 0
33
+
34
+ - name: Detect release version
35
+ id: release
36
+ env:
37
+ BEFORE_SHA: ${{ github.event.before }}
38
+ EVENT_NAME: ${{ github.event_name }}
39
+ REF_NAME: ${{ github.ref_name }}
40
+ run: |
41
+ VERSION="$(node -e 'process.stdout.write(require("./package.json").version)')"
42
+ PACKAGE_NAME="$(node -e 'process.stdout.write(require("./package.json").name)')"
43
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
44
+ echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
45
+
46
+ if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
47
+ if [ "${REF_NAME}" != "main" ]; then
48
+ echo "Manual publishing is only allowed from main."
49
+ exit 1
50
+ fi
51
+
52
+ echo "should_publish=true" >> "$GITHUB_OUTPUT"
53
+ exit 0
54
+ fi
55
+
56
+ if [ -z "${BEFORE_SHA}" ] || [ "${BEFORE_SHA}" = "0000000000000000000000000000000000000000" ]; then
57
+ echo "should_publish=true" >> "$GITHUB_OUTPUT"
58
+ exit 0
59
+ fi
60
+
61
+ PREVIOUS_VERSION="$(git show "${BEFORE_SHA}:package.json" | node -e 'let data=""; process.stdin.on("data", chunk => data += chunk); process.stdin.on("end", () => process.stdout.write(JSON.parse(data).version));')"
62
+
63
+ if [ "${PREVIOUS_VERSION}" = "${VERSION}" ]; then
64
+ echo "Version is unchanged at ${VERSION}; skipping publish."
65
+ echo "should_publish=false" >> "$GITHUB_OUTPUT"
66
+ else
67
+ echo "Version changed from ${PREVIOUS_VERSION} to ${VERSION}."
68
+ echo "should_publish=true" >> "$GITHUB_OUTPUT"
69
+ fi
70
+
71
+ - name: Setup Node
72
+ if: steps.release.outputs.should_publish == 'true'
73
+ uses: actions/setup-node@v4
74
+ with:
75
+ node-version: 24
76
+ cache: npm
77
+ registry-url: https://registry.npmjs.org
78
+
79
+ - name: Install dependencies
80
+ if: steps.release.outputs.should_publish == 'true'
81
+ run: npm ci
82
+
83
+ - name: Run automated test suite
84
+ if: steps.release.outputs.should_publish == 'true'
85
+ run: npm test
86
+
87
+ - name: Check whether version already exists on npm
88
+ if: steps.release.outputs.should_publish == 'true'
89
+ id: npm-check
90
+ env:
91
+ PACKAGE_NAME: ${{ steps.release.outputs.package_name }}
92
+ VERSION: ${{ steps.release.outputs.version }}
93
+ run: |
94
+ if npm view "${PACKAGE_NAME}@${VERSION}" version >/dev/null 2>&1; then
95
+ echo "should_publish=false" >> "$GITHUB_OUTPUT"
96
+ echo "Version ${VERSION} is already published to npm."
97
+ else
98
+ echo "should_publish=true" >> "$GITHUB_OUTPUT"
99
+ echo "Version ${VERSION} is not yet published to npm."
100
+ fi
101
+
102
+ - name: Publish to npm with trusted publishing
103
+ if: steps.release.outputs.should_publish == 'true' && steps.npm-check.outputs.should_publish == 'true'
104
+ run: npm publish --provenance --access public
105
+
106
+ - name: Tag published version
107
+ if: steps.release.outputs.should_publish == 'true' && steps.npm-check.outputs.should_publish == 'true'
108
+ env:
109
+ VERSION: ${{ steps.release.outputs.version }}
110
+ run: |
111
+ if git rev-parse "v${VERSION}" >/dev/null 2>&1; then
112
+ echo "Tag v${VERSION} already exists locally."
113
+ exit 0
114
+ fi
115
+
116
+ git config user.name "github-actions[bot]"
117
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
118
+ git tag "v${VERSION}"
119
+ git push origin "refs/tags/v${VERSION}"
120
+
121
+ - name: Write job summary
122
+ env:
123
+ VERSION: ${{ steps.release.outputs.version }}
124
+ SHOULD_PUBLISH: ${{ steps.release.outputs.should_publish }}
125
+ NPM_PUBLISH: ${{ steps.npm-check.outputs.should_publish }}
126
+ run: |
127
+ {
128
+ echo "## Publish summary"
129
+ echo
130
+ echo "- Version: \`${VERSION}\`"
131
+ echo "- Trigger requested publish: \`${SHOULD_PUBLISH:-false}\`"
132
+ echo "- npm publish executed: \`${NPM_PUBLISH:-false}\`"
133
+ } >> "$GITHUB_STEP_SUMMARY"
package/bin/cli.js CHANGED
@@ -364,6 +364,70 @@ if (retryFlagIndex >= 0) {
364
364
  args.splice(retryFlagIndex, retryOverride ? 2 : 1);
365
365
  }
366
366
 
367
+ // Look for --human-input / --no-human-input / --disable-human-input
368
+ let humanInputOverride;
369
+ let humanInputConsumesNext = false;
370
+ const humanInputFlagIndex = args.findIndex(a =>
371
+ a === '--human-input' ||
372
+ a.startsWith('--human-input=') ||
373
+ a === '--human_input' ||
374
+ a.startsWith('--human_input=') ||
375
+ a === '--no-human-input' ||
376
+ a === '--disable-human-input'
377
+ );
378
+ if (humanInputFlagIndex >= 0) {
379
+ const rawArg = args[humanInputFlagIndex];
380
+ if (rawArg.includes('=')) {
381
+ humanInputOverride = parseBool(rawArg.split('=')[1]);
382
+ } else if (rawArg === '--no-human-input' || rawArg === '--disable-human-input') {
383
+ humanInputOverride = false;
384
+ } else if (args[humanInputFlagIndex + 1] && !args[humanInputFlagIndex + 1].startsWith('-')) {
385
+ humanInputOverride = parseBool(args[humanInputFlagIndex + 1]);
386
+ humanInputConsumesNext = true;
387
+ } else {
388
+ humanInputOverride = true;
389
+ }
390
+
391
+ if (humanInputOverride !== null) {
392
+ process.env.TESTRONAUT_HUMAN_INPUT = humanInputOverride ? '1' : '0';
393
+ console.log(`👤 Human input tool ${humanInputOverride ? 'enabled' : 'disabled'} (TESTRONAUT_HUMAN_INPUT=${process.env.TESTRONAUT_HUMAN_INPUT})`);
394
+ } else {
395
+ console.warn('⚠️ Invalid --human-input value. Use true/false, 1/0, yes/no.');
396
+ }
397
+
398
+ const consume = 1 + (humanInputConsumesNext && humanInputOverride !== null ? 1 : 0);
399
+ args.splice(humanInputFlagIndex, consume);
400
+ }
401
+
402
+ // Look for --human-input-timeout / --human_input_timeout in seconds
403
+ let humanInputTimeoutOverride;
404
+ const humanInputTimeoutFlagIndex = args.findIndex(a =>
405
+ a === '--human-input-timeout' ||
406
+ a.startsWith('--human-input-timeout=') ||
407
+ a === '--human_input_timeout' ||
408
+ a.startsWith('--human_input_timeout=')
409
+ );
410
+ if (humanInputTimeoutFlagIndex >= 0) {
411
+ const rawArg = args[humanInputTimeoutFlagIndex];
412
+ if (rawArg.includes('=')) {
413
+ humanInputTimeoutOverride = rawArg.split('=')[1];
414
+ } else if (args[humanInputTimeoutFlagIndex + 1]) {
415
+ humanInputTimeoutOverride = args[humanInputTimeoutFlagIndex + 1];
416
+ }
417
+
418
+ if (humanInputTimeoutOverride) {
419
+ const n = Number(humanInputTimeoutOverride.trim());
420
+ if (Number.isFinite(n) && n > 0) {
421
+ process.env.TESTRONAUT_HUMAN_INPUT_TIMEOUT_SECONDS = String(n);
422
+ console.log(`⏱️ Human input timeout override: ${process.env.TESTRONAUT_HUMAN_INPUT_TIMEOUT_SECONDS}s`);
423
+ } else {
424
+ console.warn(`⚠️ Invalid --human-input-timeout value "${humanInputTimeoutOverride}". Ignoring.`);
425
+ }
426
+ }
427
+
428
+ args.splice(humanInputTimeoutFlagIndex, humanInputTimeoutOverride ? 2 : 1);
429
+ }
430
+
367
431
  const allResults = [];
368
432
  const runId = `run_${Date.now()}`;
369
433
  const startTime = new Date();
@@ -386,6 +450,9 @@ Options:
386
450
  --provider=<id> Override LLM provider for this run (e.g., --provider=openai)
387
451
  --dev Use the staging API base URL
388
452
  --vercel-bypass=<secret> Send Vercel protection bypass header for protected deployments
453
+ --human-input[=<bool>] Allow the agent to pause for short verification codes (default: true)
454
+ --no-human-input Disable human-in-the-loop verification prompts for automated runs
455
+ --human-input-timeout=<s> Override human input wait timeout in seconds (default: 60)
389
456
  --help Show this help message
390
457
  --retry_limit=<n> Override agent turn retry limits (minimum 1, maximum 10)
391
458
 
package/bin/init.js CHANGED
@@ -141,6 +141,7 @@ export async function initializeTestronautProject() {
141
141
  config.maxTurns = config.maxTurns ?? defaults.maxTurns;
142
142
  config.dom = { ...defaults.dom, ...(config.dom || {}) };
143
143
  config.resourceGuard = { ...defaults.resourceGuard, ...(config.resourceGuard || {}) };
144
+ config.humanInput = { ...defaults.humanInput, ...(config.humanInput || {}) };
144
145
 
145
146
  // ─────────────────────────────────────────────
146
147
  // STEP 4: Persist config and scaffold .env (first run only)
@@ -60,6 +60,10 @@ export function defaultConfig(rootBasename) {
60
60
  hrefIncludes: ['/document/', '/file/', '/download', '/attachment/'],
61
61
  dataTypes: ['document', 'file', 'item', 'row'],
62
62
  },
63
+ humanInput: {
64
+ enabled: true,
65
+ timeoutSeconds: 60,
66
+ },
63
67
  };
64
68
  }
65
69
 
package/core/agent.js CHANGED
@@ -36,7 +36,7 @@ import path from 'path';
36
36
  * @param {string} missionName
37
37
  * @param {number} [maxTurns=20] - upper bound for turnLoop per goal
38
38
  * @param {number} [retryLimit]
39
- * @param {{ domListLimit?: number|typeof Infinity, debug?: boolean, resourceGuard?: { enabled:boolean, hrefIncludes:string[], dataTypes:string[] } }} [opts]
39
+ * @param {{ domListLimit?: number|typeof Infinity, debug?: boolean, resourceGuard?: { enabled:boolean, hrefIncludes:string[], dataTypes:string[] }, humanInput?: { enabled:boolean, timeoutSeconds:number } }} [opts]
40
40
  * @returns {Promise<Array<{missionName:string, submissionType:string, submissionName:string|null, status:'passed'|'failed', steps:any[], startTime:number, endTime:number}>>}
41
41
  */
42
42
  export async function runAgent(goals, missionName, maxTurns = 20, retryLimit, opts = {}) {
@@ -135,6 +135,17 @@ Use Ground Control as your persistent mission memory about:
135
135
  - Any constraints about staying on the correct site.
136
136
  `.trim();
137
137
 
138
+ if (opts.humanInput?.enabled !== false) {
139
+ systemContent +=
140
+ '\n\n' +
141
+ `
142
+ Human-in-the-loop verification:
143
+ - If the app requires a TOTP, SMS, email, or similar short verification code that you cannot retrieve yourself, call request_human_input.
144
+ - Do not ask for passwords, API keys, or long free-form text with this tool.
145
+ - After receiving the code from the tool result, enter it into the appropriate field and continue the mission.
146
+ `.trim();
147
+ }
148
+
138
149
  if (groundSummary) {
139
150
  systemContent +=
140
151
  '\n\n' +
@@ -164,6 +175,7 @@ Use Ground Control as your persistent mission memory about:
164
175
  retryLimit,
165
176
  groundControl,
166
177
  resourceGuard: opts.resourceGuard,
178
+ humanInput: opts.humanInput,
167
179
  onStep: (s) => {
168
180
  // Append each step as a JSON line
169
181
  try {
package/core/config.js CHANGED
@@ -173,6 +173,71 @@ export function getResourceGuardConfig(cfg) {
173
173
  return { enabled, hrefIncludes: finalHref, dataTypes: finalTypes };
174
174
  }
175
175
 
176
+ function parseBool(raw) {
177
+ if (raw === undefined || raw === null) return null;
178
+ const lower = String(raw).trim().toLowerCase();
179
+ if (['1', 'true', 'yes', 'on', 'allow', 'enabled'].includes(lower)) return true;
180
+ if (['0', 'false', 'no', 'off', 'deny', 'disabled'].includes(lower)) return false;
181
+ return null;
182
+ }
183
+
184
+ /**
185
+ * Resolve human-in-the-loop verification input settings.
186
+ * Priority: CLI/env overrides → testronaut-config.json → built-in defaults.
187
+ *
188
+ * Supported config:
189
+ * humanInput: { enabled: true, timeoutSeconds: 60 }
190
+ * humanInputEnabled / allowHumanInput
191
+ * humanInputTimeoutSeconds / humanInputTimeout
192
+ *
193
+ * Supported env (set by CLI flags too):
194
+ * TESTRONAUT_HUMAN_INPUT=true|false
195
+ * TESTRONAUT_HUMAN_INPUT_TIMEOUT_SECONDS=60
196
+ *
197
+ * @param {object} cfg
198
+ * @param {{ enabled?: boolean, timeoutSeconds?: number }} [fallback]
199
+ * @returns {{ enabled:boolean, timeoutSeconds:number, source:{ enabled:'env'|'config'|'default', timeout:'env'|'config'|'default' }, clamped:boolean }}
200
+ */
201
+ export function getHumanInputConfig(cfg, fallback = { enabled: true, timeoutSeconds: 60 }) {
202
+ const clampTimeout = (raw, fb) => {
203
+ const n = Number(raw);
204
+ if (!Number.isFinite(n)) return { value: fb, clamped: false };
205
+ const clamped = Math.min(300, Math.max(5, Math.trunc(n)));
206
+ return { value: clamped, clamped: clamped !== n };
207
+ };
208
+
209
+ const envEnabled = parseBool(process.env.TESTRONAUT_HUMAN_INPUT);
210
+ const cfgEnabled = parseBool(cfg?.humanInput?.enabled ?? cfg?.humanInputEnabled ?? cfg?.allowHumanInput);
211
+ const enabled =
212
+ envEnabled !== null ? envEnabled :
213
+ cfgEnabled !== null ? cfgEnabled :
214
+ fallback.enabled !== false;
215
+ const enabledSource =
216
+ envEnabled !== null ? 'env' :
217
+ cfgEnabled !== null ? 'config' :
218
+ 'default';
219
+
220
+ const cfgTimeoutRaw = cfg?.humanInput?.timeoutSeconds ?? cfg?.humanInput?.timeout ?? cfg?.humanInputTimeoutSeconds ?? cfg?.humanInputTimeout;
221
+ const envTimeoutRaw = process.env.TESTRONAUT_HUMAN_INPUT_TIMEOUT_SECONDS;
222
+ let timeoutSource = 'default';
223
+ let timeoutRaw = fallback.timeoutSeconds ?? 60;
224
+ if (envTimeoutRaw !== undefined) {
225
+ timeoutRaw = envTimeoutRaw;
226
+ timeoutSource = 'env';
227
+ } else if (cfgTimeoutRaw !== undefined) {
228
+ timeoutRaw = cfgTimeoutRaw;
229
+ timeoutSource = 'config';
230
+ }
231
+
232
+ const timeout = clampTimeout(timeoutRaw, 60);
233
+ return {
234
+ enabled,
235
+ timeoutSeconds: timeout.value,
236
+ source: { enabled: enabledSource, timeout: timeoutSource },
237
+ clamped: timeout.clamped,
238
+ };
239
+ }
240
+
176
241
  /**
177
242
  * Baseline limits for mission runs. Adjust here for global defaults.
178
243
  *
package/core/redaction.js CHANGED
@@ -44,8 +44,13 @@ export const SENSITIVE_KEYWORDS = [
44
44
  'auth',
45
45
  'pin',
46
46
  'otp',
47
+ 'code',
48
+ 'totp',
49
+ 'mfa',
47
50
  ];
48
51
 
52
+ const VERIFICATION_CODE_KEYWORDS = new Set(['otp', 'code', 'totp', 'mfa']);
53
+
49
54
  /**
50
55
  * Builds a single RegExp that treats hyphens as `[-_\s]?` to catch variants.
51
56
  * @returns {RegExp}
@@ -141,6 +146,13 @@ function isSensitiveCall(fnName, args = {}) {
141
146
  export function redactArgs(fnName, args = {}, { showLength = true } = {}) {
142
147
  const out = clone(args);
143
148
 
149
+ if (fnName === 'request_human_input') {
150
+ for (const key of ['value', 'code', 'input', 'answer', 'redactedValue']) {
151
+ if (key in out) out[key] = maskPreview(out[key], showLength);
152
+ }
153
+ return out;
154
+ }
155
+
144
156
  // Mask direct props with sensitive-looking keys
145
157
  for (const k of Object.keys(out || {})) {
146
158
  if (hasSensitiveHint(k)) {
@@ -165,8 +177,8 @@ export function redactArgs(fnName, args = {}, { showLength = true } = {}) {
165
177
  * 1) Sensitive-word ... "VALUE"
166
178
  * e.g., `password is "hunter2"`
167
179
  *
168
- * 2) Sensitive-word ... (with|as|to|=) VALUE
169
- * e.g., `token=abcd1234`, `auth with xyz`
180
+ * 2) Sensitive-word ... (with|as|to|=|entered|provided|used|submitted|filled) VALUE
181
+ * e.g., `token=abcd1234`, `auth with xyz`, `MFA (entered 123456)`
170
182
  *
171
183
  * 3) "VALUE" ... into/in/on/to TARGET (TARGET contains a sensitive hint)
172
184
  * e.g., `Type "Imp0st3r123!" into #access`
@@ -179,18 +191,29 @@ export function redactPasswordInText(text = '', { showLength = true } = {}) {
179
191
  let s = String(text ?? '');
180
192
 
181
193
  // Build a reusable source pattern for SENSITIVE_KEYWORDS with hyphen expansion
182
- const kwParts = SENSITIVE_KEYWORDS.map(k => k.replace(/-/g, '[-_\\s]?'));
183
- const kwGroup = `(${kwParts.join('|')})`;
194
+ const strongKwParts = SENSITIVE_KEYWORDS
195
+ .filter(k => !VERIFICATION_CODE_KEYWORDS.has(k))
196
+ .map(k => k.replace(/-/g, '[-_\\s]?'));
197
+ const verificationKwParts = [...VERIFICATION_CODE_KEYWORDS].map(k => k.replace(/-/g, '[-_\\s]?'));
198
+ const strongKwGroup = `(${strongKwParts.join('|')})`;
199
+ const verificationKwGroup = `(${verificationKwParts.join('|')})`;
184
200
 
185
201
  // Case 1: keyword ... "VALUE"
186
202
  s = s.replace(
187
- new RegExp(`(\\b${kwGroup}\\b[^"'\\\\\\n]{0,80}["'])([^"']+)(["'])`, 'gi'),
203
+ new RegExp(`(\\b${strongKwGroup}\\b[^"'\\\\\\n]{0,80}(?:[:=]|\\s)["'])([^"'\\n]+)(["'])`, 'gi'),
188
204
  (_m, pre, _kw, secret, post) => pre + maskPreview(secret, showLength) + post
189
205
  );
190
206
 
191
- // Case 2: keyword ... (with|as|to|=) VALUE
207
+ // Case 2: keyword ... (with|as|to|=|entered|provided|used|submitted|filled) VALUE
208
+ s = s.replace(
209
+ new RegExp(`(\\b${strongKwGroup}\\b[^.\\n]{0,80}?(?:\\b(with|as|to|entered|provided|used|submitted|filled)\\b|=)\\s*\\(?)([^\\s"'\\\`),.;:]+)`, 'gi'),
210
+ (_m, pre, _kw1, _kw2, secret) => pre + maskPreview(secret, showLength)
211
+ );
212
+
213
+ // Verification-code words are common UI text. Only redact them when the
214
+ // phrasing says a concrete value was entered/provided/etc.
192
215
  s = s.replace(
193
- new RegExp(`(\\b${kwGroup}\\b[^.\\n]{0,80}?\\b(with|as|to|=)\\s*)([^\\s"'\\\`]+)`, 'gi'),
216
+ new RegExp(`(\\b${verificationKwGroup}\\b[^.\\n]{0,80}?\\b(entered|provided|used|submitted|filled)\\b\\s*\\(?)([A-Za-z0-9_-]{4,64})`, 'gi'),
194
217
  (_m, pre, _kw1, _kw2, secret) => pre + maskPreview(secret, showLength)
195
218
  );
196
219