testronaut 1.3.7 โ 1.4.1
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/.github/dependabot.yml +11 -0
- package/.github/workflows/periapsis-vulnerability-daily.yml +58 -0
- package/.github/workflows/periapsis-vulnerability-exception-request.yml +86 -0
- package/.github/workflows/periapsis-vulnerability-pr-check.yml +23 -0
- package/README.md +71 -7
- package/TODO.md +15 -0
- package/bin/cli.js +117 -21
- package/core/agent.js +18 -5
- package/core/turnLoop.js +162 -2
- package/package.json +5 -4
- package/policy/vulnerability-exceptions.json +36 -0
- package/policy/vulnerability-notifications.json +1 -0
- package/policy/vulnerability-owners.json +1 -0
- package/policy/vulnerability-policy.json +33 -0
- package/tests/binTests/cli.helpers.test.js +55 -0
- package/tests/coreTests/turnLoop.test.js +114 -0
- package/tests/toolsTests/mfaCode.test.js +445 -0
- package/tests/toolsTests/toolSchema.test.js +8 -0
- package/tools/chromeBrowser.js +2 -0
- package/tools/mfaCode.js +637 -0
- package/tools/toolSchema.js +28 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
+
directory: "/" # Location of package manifests
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Periapsis Vulnerability Daily Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "0 14 * * *"
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
security-events: read
|
|
11
|
+
pull-requests: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
periapsis-vulnerability:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- name: Install periapsis
|
|
19
|
+
run: npm install -g periapsis
|
|
20
|
+
- name: Run vulnerability governance check
|
|
21
|
+
run: periapsis vulnerability check --report
|
|
22
|
+
env:
|
|
23
|
+
GITHUB_TOKEN: ${{ secrets.PERIAPSIS_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
|
24
|
+
- name: Upload vulnerability report
|
|
25
|
+
if: always()
|
|
26
|
+
uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: vulnerability-report
|
|
29
|
+
path: policy/reports/
|
|
30
|
+
retention-days: 90
|
|
31
|
+
- name: Send vulnerability notifications
|
|
32
|
+
run: periapsis vulnerability notify
|
|
33
|
+
env:
|
|
34
|
+
GITHUB_TOKEN: ${{ secrets.PERIAPSIS_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
|
35
|
+
PERIAPSIS_SLACK_WEBHOOK: ${{ secrets.PERIAPSIS_SLACK_WEBHOOK }}
|
|
36
|
+
- name: Read breach count from report
|
|
37
|
+
id: report
|
|
38
|
+
run: |
|
|
39
|
+
REPORT=$(ls policy/reports/vulnerability-report-*.json 2>/dev/null | sort | tail -n1)
|
|
40
|
+
if [ -z "$REPORT" ]; then
|
|
41
|
+
echo "breached=0" >> $GITHUB_OUTPUT
|
|
42
|
+
else
|
|
43
|
+
BREACHED=$(node -e "const r=JSON.parse(require('fs').readFileSync('$REPORT','utf8')); process.stdout.write(String(r.summary.breached))")
|
|
44
|
+
echo "breached=$BREACHED" >> $GITHUB_OUTPUT
|
|
45
|
+
echo "md=${REPORT%.json}.md" >> $GITHUB_OUTPUT
|
|
46
|
+
fi
|
|
47
|
+
- name: Send breach notification email
|
|
48
|
+
if: steps.report.outputs.breached != '0'
|
|
49
|
+
uses: dawidd6/action-send-mail@v3
|
|
50
|
+
with:
|
|
51
|
+
server_address: ${{ secrets.PERIAPSIS_SMTP_HOST }}
|
|
52
|
+
server_port: ${{ secrets.PERIAPSIS_SMTP_PORT || 587 }}
|
|
53
|
+
username: ${{ secrets.PERIAPSIS_SMTP_USER }}
|
|
54
|
+
password: ${{ secrets.PERIAPSIS_SMTP_PASSWORD }}
|
|
55
|
+
subject: "[${{ github.repository }}] ${{ steps.report.outputs.breached }} vulnerability SLA breach(es) detected"
|
|
56
|
+
to: ${{ secrets.PERIAPSIS_EMAIL_TO }}
|
|
57
|
+
from: ${{ secrets.PERIAPSIS_EMAIL_FROM || secrets.PERIAPSIS_SMTP_USER }}
|
|
58
|
+
body: file://${{ steps.report.outputs.md }}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
name: Periapsis Vulnerability Exception Request
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
repo:
|
|
7
|
+
description: "Repository, e.g. owner/repo"
|
|
8
|
+
required: true
|
|
9
|
+
alertNumber:
|
|
10
|
+
description: "Dependabot alert number"
|
|
11
|
+
required: false
|
|
12
|
+
exceptionType:
|
|
13
|
+
description: "alert, package, repo_severity, ecosystem"
|
|
14
|
+
required: true
|
|
15
|
+
package:
|
|
16
|
+
description: "Package name"
|
|
17
|
+
required: false
|
|
18
|
+
ecosystem:
|
|
19
|
+
description: "Package ecosystem"
|
|
20
|
+
required: false
|
|
21
|
+
dependencyScope:
|
|
22
|
+
description: "prod, dev, unknown"
|
|
23
|
+
required: false
|
|
24
|
+
severities:
|
|
25
|
+
description: "Comma-separated severities"
|
|
26
|
+
required: true
|
|
27
|
+
reason:
|
|
28
|
+
description: "Reason for exception"
|
|
29
|
+
required: true
|
|
30
|
+
acceptedUntil:
|
|
31
|
+
description: "Expiry date YYYY-MM-DD or ISO datetime"
|
|
32
|
+
required: true
|
|
33
|
+
approvedBy:
|
|
34
|
+
description: "Approver"
|
|
35
|
+
required: true
|
|
36
|
+
evidenceRef:
|
|
37
|
+
description: "Evidence reference"
|
|
38
|
+
required: true
|
|
39
|
+
|
|
40
|
+
permissions:
|
|
41
|
+
contents: write
|
|
42
|
+
pull-requests: write
|
|
43
|
+
|
|
44
|
+
jobs:
|
|
45
|
+
exception-request:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- name: Install periapsis
|
|
50
|
+
run: npm install -g periapsis
|
|
51
|
+
- name: Add vulnerability exception
|
|
52
|
+
env:
|
|
53
|
+
INPUT_REPO: ${{ inputs.repo }}
|
|
54
|
+
INPUT_ALERT_NUMBER: ${{ inputs.alertNumber }}
|
|
55
|
+
INPUT_TYPE: ${{ inputs.exceptionType }}
|
|
56
|
+
INPUT_PACKAGE: ${{ inputs.package }}
|
|
57
|
+
INPUT_ECOSYSTEM: ${{ inputs.ecosystem }}
|
|
58
|
+
INPUT_SCOPE: ${{ inputs.dependencyScope }}
|
|
59
|
+
INPUT_SEVERITIES: ${{ inputs.severities }}
|
|
60
|
+
INPUT_REASON: ${{ inputs.reason }}
|
|
61
|
+
INPUT_UNTIL: ${{ inputs.acceptedUntil }}
|
|
62
|
+
INPUT_APPROVED_BY: ${{ inputs.approvedBy }}
|
|
63
|
+
INPUT_EVIDENCE: ${{ inputs.evidenceRef }}
|
|
64
|
+
run: |
|
|
65
|
+
periapsis vulnerability exceptions add \
|
|
66
|
+
--non-interactive \
|
|
67
|
+
--repo "$INPUT_REPO" \
|
|
68
|
+
--alert-number "$INPUT_ALERT_NUMBER" \
|
|
69
|
+
--type "$INPUT_TYPE" \
|
|
70
|
+
${INPUT_PACKAGE:+--package "$INPUT_PACKAGE"} \
|
|
71
|
+
${INPUT_ECOSYSTEM:+--ecosystem "$INPUT_ECOSYSTEM"} \
|
|
72
|
+
${INPUT_SCOPE:+--dependency-scope "$INPUT_SCOPE"} \
|
|
73
|
+
--severities "$INPUT_SEVERITIES" \
|
|
74
|
+
--reason "$INPUT_REASON" \
|
|
75
|
+
--accepted-until "$INPUT_UNTIL" \
|
|
76
|
+
--approved-by "$INPUT_APPROVED_BY" \
|
|
77
|
+
--evidence-ref "$INPUT_EVIDENCE"
|
|
78
|
+
- name: Validate vulnerability policy
|
|
79
|
+
run: periapsis vulnerability validate
|
|
80
|
+
- name: Create pull request
|
|
81
|
+
uses: peter-evans/create-pull-request@v6
|
|
82
|
+
with:
|
|
83
|
+
commit-message: "Add vulnerability SLA exception"
|
|
84
|
+
title: "Add vulnerability SLA exception"
|
|
85
|
+
body: "Adds a structured Periapsis vulnerability exception. Review required before merge."
|
|
86
|
+
branch: "periapsis/vulnerability-exception-request"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Periapsis Vulnerability PR Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
security-events: read
|
|
10
|
+
pull-requests: read
|
|
11
|
+
checks: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
periapsis-vulnerability-pr-check:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- name: Install periapsis
|
|
19
|
+
run: npm install -g periapsis
|
|
20
|
+
- name: Run vulnerability PR check
|
|
21
|
+
run: periapsis vulnerability pr-check
|
|
22
|
+
env:
|
|
23
|
+
GITHUB_TOKEN: ${{ secrets.PERIAPSIS_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ Includes:
|
|
|
44
44
|
|
|
45
45
|
## ๐ฆ Installation
|
|
46
46
|
|
|
47
|
+
**Global install** (recommended โ use `testronaut` directly):
|
|
47
48
|
```bash
|
|
48
49
|
npm install -g testronaut
|
|
49
50
|
```
|
|
@@ -51,11 +52,17 @@ npm install -g testronaut
|
|
|
51
52
|
Then initialize your project:
|
|
52
53
|
|
|
53
54
|
```bash
|
|
54
|
-
|
|
55
|
+
testronaut --init
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
Run the sample mission:
|
|
58
59
|
```bash
|
|
60
|
+
testronaut welcome.mission.js
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**One-off / no install** (use `npx` to run without installing):
|
|
64
|
+
```bash
|
|
65
|
+
npx testronaut --init
|
|
59
66
|
npx testronaut welcome.mission.js
|
|
60
67
|
```
|
|
61
68
|
|
|
@@ -117,10 +124,10 @@ Choose your preferred LLM at init or via environment variables.
|
|
|
117
124
|
|
|
118
125
|
```bash
|
|
119
126
|
# During init
|
|
120
|
-
|
|
127
|
+
testronaut --init
|
|
121
128
|
|
|
122
129
|
# Or override anytime
|
|
123
|
-
TESTRONAUT_PROVIDER=gemini TESTRONAUT_MODEL=gemini-2.5-pro
|
|
130
|
+
TESTRONAUT_PROVIDER=gemini TESTRONAUT_MODEL=gemini-2.5-pro testronaut
|
|
124
131
|
```
|
|
125
132
|
|
|
126
133
|
Current supported providers:
|
|
@@ -138,12 +145,12 @@ More providers coming soon (Anthropic, Mistral, etc.).
|
|
|
138
145
|
|
|
139
146
|
Run all missions:
|
|
140
147
|
```bash
|
|
141
|
-
|
|
148
|
+
testronaut
|
|
142
149
|
```
|
|
143
150
|
|
|
144
151
|
Run a specific mission:
|
|
145
152
|
```bash
|
|
146
|
-
|
|
153
|
+
testronaut login.mission.js
|
|
147
154
|
```
|
|
148
155
|
|
|
149
156
|
Chain missions together:
|
|
@@ -161,12 +168,12 @@ await runMissions({
|
|
|
161
168
|
|
|
162
169
|
Use the staging API base URL:
|
|
163
170
|
```bash
|
|
164
|
-
|
|
171
|
+
testronaut --dev
|
|
165
172
|
```
|
|
166
173
|
|
|
167
174
|
If the staging deployment is protected by Vercel, pass the bypass secret:
|
|
168
175
|
```bash
|
|
169
|
-
|
|
176
|
+
testronaut --dev --vercel-bypass=YOUR_SECRET login
|
|
170
177
|
```
|
|
171
178
|
|
|
172
179
|
You can also set the bypass secret via environment variables:
|
|
@@ -175,6 +182,63 @@ export VERCEL_AUTOMATION_BYPASS_SECRET=YOUR_SECRET
|
|
|
175
182
|
# or
|
|
176
183
|
export TESTRONAUT_VERCEL_BYPASS=YOUR_SECRET
|
|
177
184
|
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## ๐ Automated MFA Codes
|
|
189
|
+
|
|
190
|
+
Testronaut can retrieve a stored TOTP MFA code from the Testronaut API during a mission. This uses the `sessionToken` saved by `testronaut login` in the project root `testronaut-config.json`.
|
|
191
|
+
|
|
192
|
+
The agent will prefer the automated `get_mfa_code` tool when an MFA nickname is known, then fall back to the manual human input tool if the code is unavailable, the entry is missing, the feature is disabled, or the API session cannot access it.
|
|
193
|
+
|
|
194
|
+
If the nickname is missing or does not match exactly, the tool can call the MFA list endpoint to see available nicknames. It will retry simple case, spacing, or punctuation mismatches, such as `rudy poo` matching `rudy-poo`.
|
|
195
|
+
|
|
196
|
+
Add a default MFA nickname to `testronaut-config.json`:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"sessionToken": "eyJ...",
|
|
201
|
+
"mfaName": "github-test-mfa"
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Or pass the nickname for a single run:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
testronaut login.mission.js -o mfa=github-test-mfa
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Mission text can also name the MFA entry:
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
export const loginMission = `
|
|
215
|
+
Log in to GitHub.
|
|
216
|
+
When prompted for MFA, use the MFA nickname github-test-mfa.
|
|
217
|
+
`;
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Use staging API endpoints with the same developer flag:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
testronaut --dev login.mission.js -o mfa=github-test-mfa
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
To inspect MFA API traffic during a run, enable debug logging:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
testronaut --debug --dev login.mission.js -o mfa=github-test-mfa
|
|
230
|
+
# or
|
|
231
|
+
TESTRONAUT_API_DEBUG=1 testronaut --dev login.mission.js -o mfa=github-test-mfa
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
This writes sanitized request and response details to `missions/mission_reports/api-debug.log`, including the resolved endpoint URL, status, content type, response keys, body preview, parsed response shape, and list endpoint nicknames. Session tokens, bypass secrets, and MFA code values are redacted.
|
|
235
|
+
|
|
236
|
+
Notes:
|
|
237
|
+
- Run `testronaut login` first so `sessionToken` exists.
|
|
238
|
+
- The CLI calls the API host, not the app host.
|
|
239
|
+
- The MFA API feature flag must be enabled.
|
|
240
|
+
- Paid access is required by the API for paid-gated MFA operations. If the API returns a payment or access error, the mission can still ask for a manual code when human input is enabled.
|
|
241
|
+
|
|
178
242
|
---
|
|
179
243
|
|
|
180
244
|
## ๐ Reports
|
package/TODO.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
## Automated MFA Follow-Ups
|
|
4
|
+
|
|
5
|
+
- [ ] Add an integration smoke test against a local or staging API fixture for `get_mfa_code`.
|
|
6
|
+
- [ ] Add a staging smoke-test checklist that covers `--dev`, `--vercel-bypass`, paid access, free-user fallback, missing nickname, and expired session token behavior.
|
|
7
|
+
- [ ] Consider a dedicated `testronaut mfa list` command to verify session token access and available MFA nicknames before running missions.
|
|
8
|
+
- [ ] Consider a lightweight MFA preflight warning when `--dev` is used without `VERCEL_AUTOMATION_BYPASS_SECRET`, `TESTRONAUT_VERCEL_BYPASS`, or `--vercel-bypass`.
|
|
9
|
+
- [ ] Consider allowing `testronaut mfa list/get` to accept `--api-base` for quick diagnostics against staging, preview, or local API hosts.
|
|
10
|
+
- [ ] Track automated MFA outcomes in reports without storing raw codes.
|
|
11
|
+
- [ ] Add explicit report/log redaction assertions for MFA codes, session tokens, and Vercel bypass secrets.
|
|
12
|
+
- [ ] Decide how long `missions/mission_reports/api-debug.log` should be retained and whether it should rotate, append per run, or be copied into each run report bundle.
|
|
13
|
+
- [ ] Decide whether `-o/--options` should support more structured formats once more run options exist.
|
|
14
|
+
- [ ] Revisit whether `request_human_input` should return the code to the model or fill fields through a dedicated browser action.
|
|
15
|
+
- [ ] Document a troubleshooting matrix for MFA failures: premium required, feature disabled, invalid session, missing nickname, non-JSON/app-shell response, not found, rate limited, and network error.
|
package/bin/cli.js
CHANGED
|
@@ -63,6 +63,27 @@ const DEV_API_BASE = 'https://staging.api.testronaut.app';
|
|
|
63
63
|
|
|
64
64
|
let args = process.argv.slice(2);
|
|
65
65
|
|
|
66
|
+
// Detect how the CLI was invoked so help text matches the actual command
|
|
67
|
+
function detectCliName(npmCommand = process.env.npm_command, argv1 = process.argv[1]) {
|
|
68
|
+
// npx sets npm_command to 'exec'
|
|
69
|
+
if (npmCommand === 'exec') return 'npx testronaut';
|
|
70
|
+
// global install: argv[1] is the bin symlink named 'testronaut'
|
|
71
|
+
if (path.basename(argv1 || '') === 'testronaut') return 'testronaut';
|
|
72
|
+
return 'npx testronaut';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isDirectInvocation(argv1 = process.argv[1], moduleUrl = import.meta.url) {
|
|
76
|
+
if (!argv1) return false;
|
|
77
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
78
|
+
try {
|
|
79
|
+
return fs.realpathSync(argv1) === fs.realpathSync(modulePath);
|
|
80
|
+
} catch {
|
|
81
|
+
return path.resolve(argv1) === path.resolve(modulePath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const cliName = detectCliName();
|
|
86
|
+
|
|
66
87
|
// Look for --dev / --developer / --developer-mode / --staging
|
|
67
88
|
const devFlagIndex = args.findIndex(a =>
|
|
68
89
|
a === '--dev' ||
|
|
@@ -76,6 +97,7 @@ if (devFlagIndex >= 0) {
|
|
|
76
97
|
console.log(`๐งช Developer mode: using ${apiBase}`);
|
|
77
98
|
args.splice(devFlagIndex, 1);
|
|
78
99
|
}
|
|
100
|
+
process.env.TESTRONAUT_API_BASE_EFFECTIVE = apiBase;
|
|
79
101
|
|
|
80
102
|
function parseVercelBypassArgs(argsList) {
|
|
81
103
|
const nextArgs = [...argsList];
|
|
@@ -138,6 +160,56 @@ function parseProviderArgs(argsList) {
|
|
|
138
160
|
return { provider, args: nextArgs, invalid };
|
|
139
161
|
}
|
|
140
162
|
|
|
163
|
+
function parseRunOptionsArgs(argsList) {
|
|
164
|
+
const nextArgs = [...argsList];
|
|
165
|
+
const options = {};
|
|
166
|
+
let invalid = false;
|
|
167
|
+
|
|
168
|
+
for (let idx = 0; idx < nextArgs.length;) {
|
|
169
|
+
const rawArg = nextArgs[idx];
|
|
170
|
+
const isInline =
|
|
171
|
+
rawArg.startsWith('--options=') ||
|
|
172
|
+
rawArg.startsWith('--option=') ||
|
|
173
|
+
rawArg.startsWith('-o=');
|
|
174
|
+
const isSeparate =
|
|
175
|
+
rawArg === '--options' ||
|
|
176
|
+
rawArg === '--option' ||
|
|
177
|
+
rawArg === '-o';
|
|
178
|
+
|
|
179
|
+
if (!isInline && !isSeparate) {
|
|
180
|
+
idx += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const rawValue = isInline
|
|
185
|
+
? rawArg.slice(rawArg.indexOf('=') + 1)
|
|
186
|
+
: nextArgs[idx + 1] && !nextArgs[idx + 1].startsWith('-')
|
|
187
|
+
? nextArgs[idx + 1]
|
|
188
|
+
: '';
|
|
189
|
+
|
|
190
|
+
if (!rawValue) {
|
|
191
|
+
invalid = true;
|
|
192
|
+
nextArgs.splice(idx, 1);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const part of rawValue.split(',')) {
|
|
197
|
+
const [keyRaw, ...valueParts] = part.split('=');
|
|
198
|
+
const key = String(keyRaw || '').trim();
|
|
199
|
+
const value = valueParts.join('=').trim();
|
|
200
|
+
if (!key || !value) {
|
|
201
|
+
invalid = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
options[key] = value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
nextArgs.splice(idx, isInline ? 1 : 2);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { options, args: nextArgs, invalid };
|
|
211
|
+
}
|
|
212
|
+
|
|
141
213
|
|
|
142
214
|
const vercelBypassResult = parseVercelBypassArgs(args);
|
|
143
215
|
if (vercelBypassResult.invalid) {
|
|
@@ -234,6 +306,9 @@ export const __test__ = {
|
|
|
234
306
|
parseVercelBypassArgs,
|
|
235
307
|
createVercelBypassHeader,
|
|
236
308
|
parseProviderArgs,
|
|
309
|
+
parseRunOptionsArgs,
|
|
310
|
+
detectCliName,
|
|
311
|
+
isDirectInvocation,
|
|
237
312
|
};
|
|
238
313
|
|
|
239
314
|
// Look for --model=<id> or --model <id>
|
|
@@ -267,6 +342,20 @@ if (providerOverride) {
|
|
|
267
342
|
console.log(`๐งฉ Provider override: ${process.env.TESTRONAUT_PROVIDER}`);
|
|
268
343
|
}
|
|
269
344
|
|
|
345
|
+
const runOptionsResult = parseRunOptionsArgs(args);
|
|
346
|
+
if (runOptionsResult.invalid) {
|
|
347
|
+
console.warn('โ ๏ธ Invalid --options value. Use key=value pairs, for example: -o mfa=github-test-mfa');
|
|
348
|
+
}
|
|
349
|
+
args = runOptionsResult.args;
|
|
350
|
+
const cliMfaName =
|
|
351
|
+
runOptionsResult.options.mfa ||
|
|
352
|
+
runOptionsResult.options.mfaName ||
|
|
353
|
+
runOptionsResult.options['mfa-name'];
|
|
354
|
+
if (cliMfaName) {
|
|
355
|
+
process.env.TESTRONAUT_MFA_NAME = String(cliMfaName).trim();
|
|
356
|
+
console.log(`๐ MFA nickname override: ${process.env.TESTRONAUT_MFA_NAME}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
270
359
|
// Look for --debug / --debug=<bool> / --no-debug
|
|
271
360
|
// Normalize CLI boolean strings into true/false/null for optional flags.
|
|
272
361
|
const parseBool = (v) => {
|
|
@@ -436,18 +525,19 @@ const HELP_TEXT = `
|
|
|
436
525
|
๐งโ๐ testronaut - Autonomous Agent Mission Runner
|
|
437
526
|
|
|
438
527
|
Usage:
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
528
|
+
${cliName} Run all missions in the ./missions directory
|
|
529
|
+
${cliName} <file> Run a specific mission file (e.g., login.mission.js)
|
|
530
|
+
${cliName} login Log in and store session token
|
|
531
|
+
${cliName} upload Upload the most recent report JSON
|
|
532
|
+
${cliName} serve Serve & open the most recent HTML report (read-only)
|
|
533
|
+
${cliName} view Alias of 'serve'
|
|
445
534
|
|
|
446
535
|
Options:
|
|
447
536
|
--init Scaffold project folders and a welcome mission
|
|
448
537
|
--turns=<n> Override max turns for this run (e.g., --turns=30)
|
|
449
538
|
--debug[=<bool>] Enable verbose debug logs (or set TESTRONAUT_DEBUG=1)
|
|
450
539
|
--provider=<id> Override LLM provider for this run (e.g., --provider=openai)
|
|
540
|
+
-o, --options key=value Set run options, such as mfa=github-test-mfa
|
|
451
541
|
--dev Use the staging API base URL
|
|
452
542
|
--vercel-bypass=<secret> Send Vercel protection bypass header for protected deployments
|
|
453
543
|
--human-input[=<bool>] Allow the agent to pause for short verification codes (default: true)
|
|
@@ -457,13 +547,14 @@ Options:
|
|
|
457
547
|
--retry_limit=<n> Override agent turn retry limits (minimum 1, maximum 10)
|
|
458
548
|
|
|
459
549
|
Examples:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
550
|
+
${cliName}
|
|
551
|
+
${cliName} login
|
|
552
|
+
${cliName} upload
|
|
553
|
+
${cliName} serve
|
|
554
|
+
${cliName} --init
|
|
465
555
|
`;
|
|
466
556
|
|
|
557
|
+
async function main() {
|
|
467
558
|
if (args.includes('--init')) {
|
|
468
559
|
await initializeTestronautProject();
|
|
469
560
|
await createWelcomeMission();
|
|
@@ -494,7 +585,7 @@ Next steps:
|
|
|
494
585
|
1. Get an API key from your AI provider
|
|
495
586
|
2. Add it to your .env file
|
|
496
587
|
3. Run your first mission:
|
|
497
|
-
|
|
588
|
+
${cliName}
|
|
498
589
|
|
|
499
590
|
๐ Docs: https://docs.testronaut.app/docs/guides/cli-auth
|
|
500
591
|
`);
|
|
@@ -611,6 +702,18 @@ fs.mkdirSync(outputDir, { recursive: true });
|
|
|
611
702
|
fs.writeFileSync(`${outputDir}/${runId}.json`, JSON.stringify(report, null, 2));
|
|
612
703
|
generateHtmlReport(report, `${outputDir}/${runId}.html`);
|
|
613
704
|
|
|
705
|
+
try {
|
|
706
|
+
if (!process.env.TN_KEEP_TMP && fs.existsSync(TMP_DIR)) {
|
|
707
|
+
fs.rmSync(TMP_DIR, { recursive: true, force: true });
|
|
708
|
+
console.log('๐งน Cleaned up temporary files.');
|
|
709
|
+
} else {
|
|
710
|
+
console.log('โ ๏ธ Skipped tmp cleanup (TN_KEEP_TMP set).');
|
|
711
|
+
}
|
|
712
|
+
} catch (err) {
|
|
713
|
+
console.warn(`โ ๏ธ Could not remove tmp folder: ${err.message}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
614
717
|
/**
|
|
615
718
|
* Parse a fetch Response safely and return JSON or throw a rich error.
|
|
616
719
|
* @param {Response} res
|
|
@@ -1113,13 +1216,6 @@ async function serveLatestReport() {
|
|
|
1113
1216
|
await openInBrowser(reportUrl).catch(() => {});
|
|
1114
1217
|
}
|
|
1115
1218
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
fs.rmSync(TMP_DIR, { recursive: true, force: true });
|
|
1119
|
-
console.log('๐งน Cleaned up temporary files.');
|
|
1120
|
-
} else {
|
|
1121
|
-
console.log('โ ๏ธ Skipped tmp cleanup (TN_KEEP_TMP set).');
|
|
1122
|
-
}
|
|
1123
|
-
} catch (err) {
|
|
1124
|
-
console.warn(`โ ๏ธ Could not remove tmp folder: ${err.message}`);
|
|
1219
|
+
if (isDirectInvocation()) {
|
|
1220
|
+
await main();
|
|
1125
1221
|
}
|
package/core/agent.js
CHANGED
|
@@ -135,15 +135,28 @@ 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
|
+
systemContent +=
|
|
139
|
+
'\n\n' +
|
|
140
|
+
`
|
|
141
|
+
Verification codes:
|
|
142
|
+
- If the app requires a TOTP/MFA code, first try get_mfa_code when an MFA nickname is known from the mission text, config, or CLI options.
|
|
143
|
+
- Use the returned value promptly. If the app rejects it as expired or invalid, call get_mfa_code once more for a fresh code and retry carefully.
|
|
144
|
+
- Never invent, guess, or reuse placeholder MFA digits. Only enter an MFA value that came from get_mfa_code or request_human_input.
|
|
145
|
+
`.trim();
|
|
146
|
+
|
|
138
147
|
if (opts.humanInput?.enabled !== false) {
|
|
139
148
|
systemContent +=
|
|
140
|
-
'\n
|
|
149
|
+
'\n' +
|
|
141
150
|
`
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
- Do not ask for passwords, API keys, or long free-form text with
|
|
145
|
-
- After receiving
|
|
151
|
+
- If get_mfa_code returns that MFA is unavailable, not configured, not found, not enabled, or the account lacks access, gracefully fall back to request_human_input when human input is enabled.
|
|
152
|
+
- Use request_human_input for SMS, email, or other short verification codes that cannot be retrieved automatically.
|
|
153
|
+
- Do not ask for passwords, API keys, or long free-form text with request_human_input.
|
|
154
|
+
- After receiving a code from either tool, enter it into the appropriate field and continue the mission.
|
|
146
155
|
`.trim();
|
|
156
|
+
} else {
|
|
157
|
+
systemContent +=
|
|
158
|
+
'\n' +
|
|
159
|
+
'Human input is disabled for this run. If get_mfa_code cannot provide a usable MFA code, report a graceful FAILURE with the reason.';
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
if (groundSummary) {
|