Add Claude Code AI agent integration plugins for ArchiveBox#7
Conversation
There was a problem hiding this comment.
4 issues found across 11 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="abx_plugins/plugins/claudechrome/on_Crawl__84_claudechrome_install.bg.js">
<violation number="1" location="abx_plugins/plugins/claudechrome/on_Crawl__84_claudechrome_install.bg.js:33">
P2: Return a non-zero exit code when the extension install returns null; otherwise crawl setup can report success even though Claude for Chrome was never installed.</violation>
</file>
<file name="abx_plugins/plugins/claudechrome/on_Snapshot__47_claudechrome.js">
<violation number="1" location="abx_plugins/plugins/claudechrome/on_Snapshot__47_claudechrome.js:202">
P1: `chrome.sidePanel.open()` is called with a DevTools target id and without a user gesture, so the side panel will not open reliably.</violation>
</file>
<file name="abx_plugins/plugins/claudechrome/templates/full.html">
<violation number="1" location="abx_plugins/plugins/claudechrome/templates/full.html:5">
P2: Remove top-level navigation from this iframe sandbox before previewing untrusted downloaded HTML.</violation>
</file>
<file name="abx_plugins/plugins/claudechrome/tests/test_claudechrome.py">
<violation number="1" location="abx_plugins/plugins/claudechrome/tests/test_claudechrome.py:171">
P1: Custom agent: **Test quality checker**
Replace this placeholder with a real end-to-end test or remove it; a `pass` test for the claimed full pipeline violates the test-quality rule.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
|
||
| // Check if enabled | ||
| if (!getEnvBool('CLAUDECHROME_ENABLED', false)) { | ||
| process.exit(0); |
There was a problem hiding this comment.
P2: Return a non-zero exit code when the extension install returns null; otherwise crawl setup can report success even though Claude for Chrome was never installed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At abx_plugins/plugins/claudechrome/on_Crawl__84_claudechrome_install.bg.js, line 33:
<comment>Return a non-zero exit code when the extension install returns null; otherwise crawl setup can report success even though Claude for Chrome was never installed.</comment>
<file context>
@@ -0,0 +1,70 @@
+
+// Check if enabled
+if (!getEnvBool('CLAUDECHROME_ENABLED', false)) {
+ process.exit(0);
+}
+
</file context>
| <iframe class="full-page-iframe" | ||
| src="{{ output_path }}" | ||
| name="preview" | ||
| sandbox="allow-top-navigation-by-user-activation"> |
There was a problem hiding this comment.
P2: Remove top-level navigation from this iframe sandbox before previewing untrusted downloaded HTML.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At abx_plugins/plugins/claudechrome/templates/full.html, line 5:
<comment>Remove top-level navigation from this iframe sandbox before previewing untrusted downloaded HTML.</comment>
<file context>
@@ -0,0 +1,6 @@
+<iframe class="full-page-iframe"
+ src="{{ output_path }}"
+ name="preview"
+ sandbox="allow-top-navigation-by-user-activation">
+</iframe>
</file context>
| if (!extPage || !extPage.url().startsWith(`chrome-extension://${extensionId}`)) { | ||
| console.warn('[!] Could not access extension context for config injection'); | ||
| // Still mark as configured so we don't retry | ||
| fs.writeFileSync(CONFIG_MARKER, JSON.stringify({ | ||
| timestamp: new Date().toISOString(), | ||
| extensionId, | ||
| configured: false, | ||
| note: 'Could not access extension context - user may need to log in manually', | ||
| }, null, 2)); | ||
| return { success: true, skipped: true }; |
There was a problem hiding this comment.
🟡 Browser page leaked when extension context is unreachable
In the config hook, when no existing extension page is found, a new page is created at line 107 via browser.newPage(). If none of the extension page URLs are successfully navigated (lines 110-121), the check at line 128 fails and the function returns at line 137 without closing the newly created page. The finally block at line 184 only calls browser.disconnect(), which disconnects Puppeteer but does not close browser tabs. This leaves an orphaned blank tab open in the persistent Chrome session, which wastes memory and could interfere with hooks that iterate over or count open browser pages.
| if (!extPage || !extPage.url().startsWith(`chrome-extension://${extensionId}`)) { | |
| console.warn('[!] Could not access extension context for config injection'); | |
| // Still mark as configured so we don't retry | |
| fs.writeFileSync(CONFIG_MARKER, JSON.stringify({ | |
| timestamp: new Date().toISOString(), | |
| extensionId, | |
| configured: false, | |
| note: 'Could not access extension context - user may need to log in manually', | |
| }, null, 2)); | |
| return { success: true, skipped: true }; | |
| if (!extPage || !extPage.url().startsWith(`chrome-extension://${extensionId}`)) { | |
| console.warn('[!] Could not access extension context for config injection'); | |
| // Close the page we opened if it's not an existing extension page | |
| try { await extPage.close(); } catch (e) {} | |
| // Still mark as configured so we don't retry | |
| fs.writeFileSync(CONFIG_MARKER, JSON.stringify({ | |
| timestamp: new Date().toISOString(), | |
| extensionId, | |
| configured: false, | |
| note: 'Could not access extension context - user may need to log in manually', | |
| }, null, 2)); | |
| return { success: true, skipped: true }; | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
| messages.push({ role: 'assistant', content: assistantContent }); | ||
| messages.push({ | ||
| role: 'user', | ||
| content: [ | ||
| { | ||
| type: 'tool_result', | ||
| tool_use_id: block.id, | ||
| content: [ | ||
| { | ||
| type: 'image', | ||
| source: { | ||
| type: 'base64', | ||
| media_type: 'image/png', | ||
| data: screenshot, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| // Only process the first tool_use per response | ||
| break; |
There was a problem hiding this comment.
🔴 Missing tool_results for multi-tool_use responses breaks API on next call
When Claude's response contains multiple tool_use blocks, line 517 pushes the full assistantContent (including all tool_use blocks) into the messages array, but lines 518-536 only append a single tool_result for the first tool_use block (then break at line 539 skips the rest). On the next iteration, callAnthropicAPI sends these messages to the Anthropic API, which requires a matching tool_result for every tool_use in the preceding assistant message. The API will reject the request with an invalid_request_error ("Missing tool_result for tool_use_id ..."), causing the agentic loop to break prematurely at line 477. While the computer_20250124 tool typically returns one action per turn, the API contract doesn't guarantee this, and the code would silently fail if multiple tool_use blocks are returned.
Was this helpful? React with 👍 or 👎 to provide feedback.
| // If stop_reason is end_turn with no tool use, we're done | ||
| if (response.stop_reason === 'end_turn' && !hasToolUse) { | ||
| break; | ||
| } |
There was a problem hiding this comment.
🟡 Dead code: second !hasToolUse break is unreachable
At lines 550-552, the loop breaks if !hasToolUse. The subsequent check at lines 556-558 (response.stop_reason === 'end_turn' && !hasToolUse) is unreachable dead code — if !hasToolUse were true, execution would have already exited via the break at line 552. This suggests the developer may have intended different logic for the first check (e.g., only logging without breaking, or checking a different condition), which could mean the loop continues iterating when it shouldn't, or breaks too early depending on original intent.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const { | ||
| getEnv, | ||
| getEnvBool, | ||
| getEnvInt, | ||
| parseArgs, | ||
| readCdpUrl, | ||
| connectToPage, | ||
| waitForPageLoaded, | ||
| } = require('../chrome/chrome_utils.js'); |
There was a problem hiding this comment.
🟡 claudechrome imports parseArgs from both base/utils.js and chrome_utils.js, causing duplicate import
In claudechrome/on_Snapshot__47_claudechrome.js:38, parseArgs is imported from chrome_utils.js, but chrome_utils.js itself re-exports the parseArgs it imports from base/utils.js. Meanwhile emitArchiveResult is correctly imported from base/utils.js at line 33. This isn't a runtime bug (the function works fine either way since it's the same function), but it's inconsistent with the pattern established by other refactored plugins in this PR (e.g., dom, pdf, screenshot) which import parseArgs from base/utils.js and chrome-specific functions from chrome_utils.js.
| const { | |
| getEnv, | |
| getEnvBool, | |
| getEnvInt, | |
| parseArgs, | |
| readCdpUrl, | |
| connectToPage, | |
| waitForPageLoaded, | |
| } = require('../chrome/chrome_utils.js'); | |
| const { | |
| getEnv, | |
| getEnvBool, | |
| getEnvInt, | |
| parseArgs, | |
| emitArchiveResult, | |
| } = require('../base/utils.js'); | |
| const { | |
| readCdpUrl, | |
| connectToPage, | |
| waitForPageLoaded, | |
| } = require('../chrome/chrome_utils.js'); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| const { emitArchiveResult } = require('../base/utils.js'); | ||
| const { | ||
| getEnv, | ||
| getEnvBool, | ||
| getEnvInt, | ||
| parseArgs, | ||
| readCdpUrl, | ||
| connectToPage, | ||
| waitForPageLoaded, | ||
| } = require('../chrome/chrome_utils.js'); |
There was a problem hiding this comment.
🟡 emitArchiveResult imported from base/utils.js but also imported duplicate getEnv/getEnvBool/getEnvInt/parseArgs from chrome_utils.js
In claudechrome/on_Snapshot__47_claudechrome.js, emitArchiveResult is imported from ../base/utils.js (line 33), but getEnv, getEnvBool, getEnvInt, and parseArgs are imported from ../chrome/chrome_utils.js (lines 35-42). While chrome_utils.js re-exports these from base/utils.js so it works at runtime, the emitArchiveResult function is NOT re-exported by chrome_utils.js — that's why it must come from base/utils.js. This inconsistency is cosmetic but could confuse future maintainers.
Was this helpful? React with 👍 or 👎 to provide feedback.
| - name: Discover test files | ||
| id: set-matrix | ||
| run: | | ||
| chrome_test_pattern='ensure_chrome_test_prereqs|ensure_chromium_and_puppeteer_installed|require_chrome_runtime|chrome_session\(|CHROME_NAVIGATE_HOOK' |
There was a problem hiding this comment.
🟡 Workflow indentation error: chrome_test_pattern variable defined outside run: block's indentation scope
In .github/workflows/test-parallel.yml:35, the chrome_test_pattern variable assignment is indented at 10 spaces while the rest of the run: block content is at 12 spaces (the plugin_tests= line at line 36 is at 12 spaces). This means chrome_test_pattern is syntactically outside the shell script body for the step. In YAML multiline run: blocks, all lines need consistent indentation relative to the block scalar indicator. The line at 10 spaces will still be included in the shell script (YAML strips leading whitespace based on the first content line's indentation), but the actual shell variable will be set correctly since bash doesn't care about indentation. So this is cosmetic rather than a runtime bug.
| chrome_test_pattern='ensure_chrome_test_prereqs|ensure_chromium_and_puppeteer_installed|require_chrome_runtime|chrome_session\(|CHROME_NAVIGATE_HOOK' | |
| chrome_test_pattern='ensure_chrome_test_prereqs|ensure_chromium_and_puppeteer_installed|require_chrome_runtime|chrome_session\(|CHROME_NAVIGATE_HOOK' |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
17 issues found across 104 files (changes from recent commits).
Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="abx_plugins/plugins/pip/on_Binary__11_pip_install.py">
<violation number="1" location="abx_plugins/plugins/pip/on_Binary__11_pip_install.py:168">
P2: Pass the chosen `LIB_DIR` into the permission lock-down helper; as written, custom `LIB_DIR` installs are left unprotected because the helper always falls back to `~/.config/abx/lib`.</violation>
</file>
<file name="abx_plugins/plugins/base/utils.js">
<violation number="1" location="abx_plugins/plugins/base/utils.js:56">
P2: Comma-splitting array env vars will corrupt arguments that legitimately contain commas, such as Chrome flags.</violation>
</file>
<file name="abx_plugins/plugins/base/utils.py">
<violation number="1" location="abx_plugins/plugins/base/utils.py:346">
P1: Permissions do not enforce the stated read-only constraint on `lib/`. `0o755` (dirs) and `0o644`/`0o755` (files) both grant the owner write access. Since the owner is set to the same uid that snapshot hooks run as, those hooks can still modify installed binaries. Use `0o555` for directories and `0o444`/`0o555` for files to actually make `lib/` read-only for the data-dir user.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:123">
P2: These documented helper signatures don't match `base/utils.py`, so copying them will call the wrong API. In particular, `output_binary(...)` is documented as an installed-binary emitter, but the real helper takes `binproviders` and emits a dependency declaration.</violation>
</file>
<file name="abx_plugins/plugins/chrome/tests/chrome_test_helpers.py">
<violation number="1" location="abx_plugins/plugins/chrome/tests/chrome_test_helpers.py:80">
P2: Import the shared test helpers via `abx_plugins.plugins.base.test_utils` instead of appending to `sys.path` and importing top-level `base`.</violation>
</file>
<file name="abx_plugins/plugins/archivedotorg/on_Snapshot__08_archivedotorg.bg.py">
<violation number="1" location="abx_plugins/plugins/archivedotorg/on_Snapshot__08_archivedotorg.bg.py:97">
P2: Preserve the robots.txt/manual-retry path for HTTPError responses. Right now blocked Wayback submissions can be reported as failed before `RobotAccessControlException` is inspected.</violation>
</file>
<file name="abx_plugins/plugins/claudechrome/tests/test_claudechrome.py">
<violation number="1" location="abx_plugins/plugins/claudechrome/tests/test_claudechrome.py:194">
P1: Custom agent: **Test quality checker**
This test no longer verifies graceful failure: it passes even when the hook crashes before emitting the required failed `ArchiveResult` JSONL.</violation>
<violation number="2" location="abx_plugins/plugins/claudechrome/tests/test_claudechrome.py:203">
P2: Guard this integration test behind an explicit API-key check. As written it runs by default, but the hook fails immediately without `ANTHROPIC_API_KEY` and otherwise makes a live Anthropic request.</violation>
<violation number="3" location="abx_plugins/plugins/claudechrome/tests/test_claudechrome.py:270">
P1: Custom agent: **Test quality checker**
This integration test is effectively a smoke test: it never verifies that Claude actually clicked `Show More` and changed the page state, so a no-op run can still pass.</violation>
</file>
<file name="abx_plugins/plugins/git/on_Snapshot__05_git.bg.py">
<violation number="1" location="abx_plugins/plugins/git/on_Snapshot__05_git.bg.py:68">
P2: Guard `load_config()` here or preserve the old fallback behavior for malformed env values. As written, an empty `GIT_TIMEOUT` or non-JSON `GIT_ARGS` now aborts the hook with `ValidationError` instead of falling back to defaults.</violation>
</file>
<file name="pyproject.toml">
<violation number="1" location="pyproject.toml:24">
P2: Removing `requests` from the main dependency set leaves the packaged test suite without a required import, so a plain install can no longer run the bundled tests successfully.</violation>
</file>
<file name="abx_plugins/plugins/defuddle/tests/test_defuddle.py">
<violation number="1" location="abx_plugins/plugins/defuddle/tests/test_defuddle.py:197">
P2: This change weakens the test: it now accepts extra JSONL records instead of verifying the hook emits exactly one failure result.</violation>
</file>
<file name=".github/workflows/test-parallel.yml">
<violation number="1" location=".github/workflows/test-parallel.yml:124">
P1: This step hardcodes the action's `/usr/bin/chromium` default instead of verifying the `CHROME_BINARY` path the tests actually provision. Chrome-dependent jobs can now fail before pytest installs Chromium.</violation>
</file>
<file name=".github/actions/verify-chromium-launch/action.yml">
<violation number="1" location=".github/actions/verify-chromium-launch/action.yml:24">
P1: Custom agent: **Test quality checker**
Fail this Chromium verifier on the first failed launch instead of retrying it to green; the retry loop violates the test-quality rule's no-flaky-tests / retry-logic clause by allowing an intermittent browser startup failure to pass CI.</violation>
<violation number="2" location=".github/actions/verify-chromium-launch/action.yml:98">
P2: Poll for the page target (or create one) instead of failing on the first `Target.getTargets` call.</violation>
<violation number="3" location=".github/actions/verify-chromium-launch/action.yml:245">
P2: Stop waiting for the full timeout after Chromium has already exited.</violation>
</file>
<file name="abx_plugins/plugins/archivedotorg/tests/test_archivedotorg.py">
<violation number="1" location="abx_plugins/plugins/archivedotorg/tests/test_archivedotorg.py:142">
P1: Custom agent: **Test quality checker**
`test_handles_timeout` no longer tests timeout handling; it now passes on any archive.org error, so regressions in the dedicated timeout path can slip through.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| for dirpath, _dirnames, filenames in os.walk(lib_dir): | ||
| dp = Path(dirpath) | ||
| _chown_if_needed(dp, target_uid, target_gid) | ||
| dp.chmod(0o755) # rwxr-xr-x |
There was a problem hiding this comment.
P1: Permissions do not enforce the stated read-only constraint on lib/. 0o755 (dirs) and 0o644/0o755 (files) both grant the owner write access. Since the owner is set to the same uid that snapshot hooks run as, those hooks can still modify installed binaries. Use 0o555 for directories and 0o444/0o555 for files to actually make lib/ read-only for the data-dir user.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At abx_plugins/plugins/base/utils.py, line 346:
<comment>Permissions do not enforce the stated read-only constraint on `lib/`. `0o755` (dirs) and `0o644`/`0o755` (files) both grant the owner write access. Since the owner is set to the same uid that snapshot hooks run as, those hooks can still modify installed binaries. Use `0o555` for directories and `0o444`/`0o555` for files to actually make `lib/` read-only for the data-dir user.</comment>
<file context>
@@ -0,0 +1,382 @@
+ for dirpath, _dirnames, filenames in os.walk(lib_dir):
+ dp = Path(dirpath)
+ _chown_if_needed(dp, target_uid, target_gid)
+ dp.chmod(0o755) # rwxr-xr-x
+ for fname in filenames:
+ fp = dp / fname
</file context>
|
|
||
| - name: Verify Chromium launch | ||
| if: ${{ matrix.test.needs_chromium }} | ||
| uses: ./.github/actions/verify-chromium-launch |
There was a problem hiding this comment.
P1: This step hardcodes the action's /usr/bin/chromium default instead of verifying the CHROME_BINARY path the tests actually provision. Chrome-dependent jobs can now fail before pytest installs Chromium.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/test-parallel.yml, line 124:
<comment>This step hardcodes the action's `/usr/bin/chromium` default instead of verifying the `CHROME_BINARY` path the tests actually provision. Chrome-dependent jobs can now fail before pytest installs Chromium.</comment>
<file context>
@@ -114,6 +119,10 @@ jobs:
+ - name: Verify Chromium launch
+ if: ${{ matrix.test.needs_chromium }}
+ uses: ./.github/actions/verify-chromium-launch
+
- name: Run test - ${{ matrix.test.name }}
</file context>
| set -euo pipefail | ||
| max_attempts="${{ inputs.max-attempts }}" | ||
| attempt=1 | ||
| while [ "$attempt" -le "$max_attempts" ]; do |
There was a problem hiding this comment.
P1: Custom agent: Test quality checker
Fail this Chromium verifier on the first failed launch instead of retrying it to green; the retry loop violates the test-quality rule's no-flaky-tests / retry-logic clause by allowing an intermittent browser startup failure to pass CI.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/actions/verify-chromium-launch/action.yml, line 24:
<comment>Fail this Chromium verifier on the first failed launch instead of retrying it to green; the retry loop violates the test-quality rule's no-flaky-tests / retry-logic clause by allowing an intermittent browser startup failure to pass CI.</comment>
<file context>
@@ -0,0 +1,279 @@
+ set -euo pipefail
+ max_attempts="${{ inputs.max-attempts }}"
+ attempt=1
+ while [ "$attempt" -le "$max_attempts" ]; do
+ if [ -n "${{ inputs.chrome-path }}" ]; then
+ pkill -f "${{ inputs.chrome-path }}" >/dev/null 2>&1 || true
</file context>
| assert result_json, "Should emit failed JSONL on error" | ||
| assert result_json["status"] == "failed", result_json | ||
| assert "timed out" in result_json["output_str"].lower(), result_json | ||
| assert result_json["output_str"], "Should include error description" |
There was a problem hiding this comment.
P1: Custom agent: Test quality checker
test_handles_timeout no longer tests timeout handling; it now passes on any archive.org error, so regressions in the dedicated timeout path can slip through.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At abx_plugins/plugins/archivedotorg/tests/test_archivedotorg.py, line 142:
<comment>`test_handles_timeout` no longer tests timeout handling; it now passes on any archive.org error, so regressions in the dedicated timeout path can slip through.</comment>
<file context>
@@ -145,17 +133,13 @@ def test_handles_timeout():
+ assert result_json, "Should emit failed JSONL on error"
assert result_json["status"] == "failed", result_json
- assert "timed out" in result_json["output_str"].lower(), result_json
+ assert result_json["output_str"], "Should include error description"
</file context>
| timeout = get_env_int("GIT_TIMEOUT") or get_env_int("TIMEOUT", 120) | ||
| git_args = get_env_array("GIT_ARGS", ["clone", "--depth=1", "--recursive"]) | ||
| git_args_extra = get_env_array("GIT_ARGS_EXTRA", []) | ||
| config = load_config() |
There was a problem hiding this comment.
P2: Guard load_config() here or preserve the old fallback behavior for malformed env values. As written, an empty GIT_TIMEOUT or non-JSON GIT_ARGS now aborts the hook with ValidationError instead of falling back to defaults.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At abx_plugins/plugins/git/on_Snapshot__05_git.bg.py, line 68:
<comment>Guard `load_config()` here or preserve the old fallback behavior for malformed env values. As written, an empty `GIT_TIMEOUT` or non-JSON `GIT_ARGS` now aborts the hook with `ValidationError` instead of falling back to defaults.</comment>
<file context>
@@ -86,9 +65,10 @@ def clone_git(url: str, binary: str) -> tuple[bool, str | None, str]:
- timeout = get_env_int("GIT_TIMEOUT") or get_env_int("TIMEOUT", 120)
- git_args = get_env_array("GIT_ARGS", ["clone", "--depth=1", "--recursive"])
- git_args_extra = get_env_array("GIT_ARGS_EXTRA", [])
+ config = load_config()
+ timeout = config.GIT_TIMEOUT
+ git_args = config.GIT_ARGS
</file context>
| "pydantic-settings>=2.0.0", | ||
| "pyright>=1.1.408", | ||
| "pytest>=9.0.2", | ||
| "pytest-httpserver>=1.1.0", |
There was a problem hiding this comment.
P2: Removing requests from the main dependency set leaves the packaged test suite without a required import, so a plain install can no longer run the bundled tests successfully.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pyproject.toml, line 24:
<comment>Removing `requests` from the main dependency set leaves the packaged test suite without a required import, so a plain install can no longer run the bundled tests successfully.</comment>
<file context>
@@ -21,10 +21,10 @@ classifiers = [
dependencies = [
"abx-pkg>=0.6.3",
"feedparser>=6.0.0",
+ "pydantic-settings>=2.0.0",
"pyright>=1.1.408",
"pytest>=9.0.2",
</file context>
| "pydantic-settings>=2.0.0", | |
| "pyright>=1.1.408", | |
| "pytest>=9.0.2", | |
| "pytest-httpserver>=1.1.0", | |
| "pydantic-settings>=2.0.0", | |
| "pyright>=1.1.408", | |
| "pytest>=9.0.2", | |
| "pytest-httpserver>=1.1.0", | |
| "requests>=2.32.5", |
| record = parse_jsonl_output(result.stdout) | ||
| assert record, "Should have ArchiveResult JSONL output" |
There was a problem hiding this comment.
P2: This change weakens the test: it now accepts extra JSONL records instead of verifying the hook emits exactly one failure result.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At abx_plugins/plugins/defuddle/tests/test_defuddle.py, line 197:
<comment>This change weakens the test: it now accepts extra JSONL records instead of verifying the hook emits exactly one failure result.</comment>
<file context>
@@ -198,11 +194,8 @@ def test_reports_missing_dependency_when_not_installed():
- ]
- assert len(jsonl_lines) == 1
- record = json.loads(jsonl_lines[0])
+ record = parse_jsonl_output(result.stdout)
+ assert record, "Should have ArchiveResult JSONL output"
assert record["type"] == "ArchiveResult"
</file context>
| record = parse_jsonl_output(result.stdout) | |
| assert record, "Should have ArchiveResult JSONL output" | |
| jsonl_lines = [ | |
| line for line in result.stdout.strip().split("\n") if line.strip().startswith("{") | |
| ] | |
| assert len(jsonl_lines) == 1 | |
| record = json.loads(jsonl_lines[0]) |
|
|
||
| ws.addEventListener("open", async () => { | ||
| try { | ||
| const targets = await send("Target.getTargets"); |
There was a problem hiding this comment.
P2: Poll for the page target (or create one) instead of failing on the first Target.getTargets call.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/actions/verify-chromium-launch/action.yml, line 98:
<comment>Poll for the page target (or create one) instead of failing on the first `Target.getTargets` call.</comment>
<file context>
@@ -0,0 +1,279 @@
+
+ ws.addEventListener("open", async () => {
+ try {
+ const targets = await send("Target.getTargets");
+ const pageTarget = (targets.targetInfos || []).find(
+ (target) => target.type === "page" && target.url === "about:blank",
</file context>
|
|
||
| proc.stdout.on("data", onData("stdout")); | ||
| proc.stderr.on("data", onData("stderr")); | ||
| proc.on("exit", (code, signal) => { |
There was a problem hiding this comment.
P2: Stop waiting for the full timeout after Chromium has already exited.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/actions/verify-chromium-launch/action.yml, line 245:
<comment>Stop waiting for the full timeout after Chromium has already exited.</comment>
<file context>
@@ -0,0 +1,279 @@
+
+ proc.stdout.on("data", onData("stdout"));
+ proc.stderr.on("data", onData("stderr"));
+ proc.on("exit", (code, signal) => {
+ if (!closed && !wsUrl) {
+ closed = true;
</file context>
There was a problem hiding this comment.
3 issues found across 20 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/test_chrome_runtime_fixture.py">
<violation number="1" location="tests/test_chrome_runtime_fixture.py:6">
P3: Use pytest's public `pytest.fail.Exception` instead of importing the private `_pytest.outcomes.Failed` type.</violation>
<violation number="2" location="tests/test_chrome_runtime_fixture.py:19">
P1: Custom agent: **Test quality checker**
These tests violate the test-quality rule by monkeypatching `Binary.load` and calling `require_chrome_runtime.__wrapped__()`, so they only verify mocked fixture internals instead of the real runtime-resolution behavior.</violation>
</file>
<file name="abx_plugins/plugins/chrome/chrome_utils.js">
<violation number="1" location="abx_plugins/plugins/chrome/chrome_utils.js:1507">
P2: Use the default lib directory here instead of requiring `LIB_DIR` to be set, or hook-installed Chromium in `~/.config/abx/lib` will not be discovered.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| loaded.append(self.name) | ||
| return self | ||
|
|
||
| monkeypatch.setattr(Binary, "load", fake_load) |
There was a problem hiding this comment.
P1: Custom agent: Test quality checker
These tests violate the test-quality rule by monkeypatching Binary.load and calling require_chrome_runtime.__wrapped__(), so they only verify mocked fixture internals instead of the real runtime-resolution behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test_chrome_runtime_fixture.py, line 19:
<comment>These tests violate the test-quality rule by monkeypatching `Binary.load` and calling `require_chrome_runtime.__wrapped__()`, so they only verify mocked fixture internals instead of the real runtime-resolution behavior.</comment>
<file context>
@@ -0,0 +1,40 @@
+ loaded.append(self.name)
+ return self
+
+ monkeypatch.setattr(Binary, "load", fake_load)
+
+ conftest.require_chrome_runtime.__wrapped__()
</file context>
| import conftest | ||
|
|
||
| import pytest | ||
| from _pytest.outcomes import Failed |
There was a problem hiding this comment.
P3: Use pytest's public pytest.fail.Exception instead of importing the private _pytest.outcomes.Failed type.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test_chrome_runtime_fixture.py, line 6:
<comment>Use pytest's public `pytest.fail.Exception` instead of importing the private `_pytest.outcomes.Failed` type.</comment>
<file context>
@@ -0,0 +1,40 @@
+import conftest
+
+import pytest
+from _pytest.outcomes import Failed
+
+from abx_pkg import Binary
</file context>
| except HTTPError as e: | ||
| if e.code >= 400: | ||
| return False, None, f"HTTP {e.code}" | ||
| return False, None, f"HTTPError: {e}" |
There was a problem hiding this comment.
🔴 archivedotorg: Migration from requests to urllib causes robots.txt-blocked URLs to be treated as hard failures
The migration from requests.get() to urllib.request.urlopen() changes how HTTP 4xx/5xx responses are handled. urlopen() raises HTTPError for 4xx/5xx status codes before the response body can be inspected, while requests.get() returns the response object regardless of status code. The old code checked the response body for RobotAccessControlException (line 86 in new code) before checking the status code, treating robots.txt-blocked URLs as soft successes (saving the submit URL for manual retry). With urlopen, a 403 from archive.org's robots.txt blocking raises HTTPError at line 58, which is caught at line 97 and returned as a hard failure (False, None, "HTTP 403"). The RobotAccessControlException body check at line 86 is now unreachable for 4xx/5xx responses.
Fix approach
Read the response body from the HTTPError object (which is file-like) and check for RobotAccessControlException before returning a hard failure:
except HTTPError as e:
body = e.read().decode('utf-8', errors='replace')
if 'RobotAccessControlException' in body:
Path(OUTPUT_FILE).write_text(submit_url, encoding='utf-8')
log('Blocked by robots.txt, saved submit URL for manual retry')
return True, OUTPUT_FILE, ''
if e.code >= 400:
return False, None, f'HTTP {e.code}'| except HTTPError as e: | |
| if e.code >= 400: | |
| return False, None, f"HTTP {e.code}" | |
| return False, None, f"HTTPError: {e}" | |
| except HTTPError as e: | |
| err_body = e.read().decode("utf-8", errors="replace") | |
| if "RobotAccessControlException" in err_body: | |
| Path(OUTPUT_FILE).write_text(submit_url, encoding="utf-8") | |
| log("Blocked by robots.txt, saved submit URL for manual retry") | |
| return True, OUTPUT_FILE, "" # Consider this a soft success | |
| if e.code >= 400: | |
| return False, None, f"HTTP {e.code}" | |
| return False, None, f"HTTPError: {e}" |
Was this helpful? React with 👍 or 👎 to provide feedback.
| except TimeoutError: | ||
| return False, None, f"Request timed out after {timeout} seconds" |
There was a problem hiding this comment.
🟡 Unreachable except TimeoutError handler — urllib wraps timeouts in URLError
After migrating from requests to urllib, the except TimeoutError clause is dead code. When urllib.request.urlopen encounters a socket timeout, it wraps the socket.timeout (which IS TimeoutError) inside a URLError via its internal except OSError as err: raise URLError(err) handler (urllib/request.py:do_open). The raised exception is URLError, not TimeoutError, so the except TimeoutError at line 101 never fires. Timeouts are instead caught by except URLError as e at line 103, producing the message "URLError: <urlopen error timed out>" instead of the intended user-friendly "Request timed out after {timeout} seconds".
| except TimeoutError: | |
| return False, None, f"Request timed out after {timeout} seconds" | |
| except URLError as e: | |
| if isinstance(e.reason, TimeoutError): | |
| return False, None, f"Request timed out after {timeout} seconds" | |
| if isinstance(e.reason, HTTPError) or (hasattr(e, 'code') and e.code >= 400): | |
| return False, None, f"HTTP error: {e}" | |
| return False, None, f"URLError: {e.reason}" |
Was this helpful? React with 👍 or 👎 to provide feedback.
- claudecode: Base plugin that installs Claude Code CLI from npm and provides shared utilities for spawning Claude Code with system prompts describing the crawl/snapshot directory layout and metadata. Configurable via ANTHROPIC_API_KEY, CLAUDECODE_MODEL, CLAUDECODE_MAX_TURNS, etc. - claudecodeextract: Runs at snapshot priority 58 with a user-configurable prompt (CLAUDECODEEXTRACT_PROMPT) to generate derived content from existing extractor outputs. Default prompt generates clean Markdown from best available source. - claudecodecleanup: Runs at snapshot priority 99 (end of pipeline) to analyze all extractor outputs, identify duplicate/redundant files, compare quality, and delete inferior versions. Configurable via CLAUDECODECLEANUP_PROMPT. All three follow the established plugin patterns (config.json, templates, install hooks, snapshot hooks, emit_archive_result JSONL output). https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
…ugins 28 tests covering: - Hook scripts exist and are executable - Config JSON schemas are valid with correct properties - Plugin dependencies declared (required_plugins: claudecode) - Install hook emits correct Binary JSONL for @anthropic-ai/claude-code - Hooks skip cleanly when ENABLED=false - Hooks fail gracefully with missing API key or binary - System prompt builder includes snapshot metadata and extractor outputs - Cleanup hook runs at priority 99 with higher max_turns default https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
Add integration test classes that actually run Claude Code CLI against real snapshot data when ANTHROPIC_API_KEY is available: - TestClaudeCodeIntegration: tests simple prompt response, system prompt with snapshot context, and file writing via Claude Code - TestClaudeCodeExtractIntegration: runs full extract hook against a fake snapshot with readability/htmltotext/dom outputs, tests default markdown generation and custom prompt (JSON extraction) - TestClaudeCodeCleanupIntegration: runs cleanup hook against a snapshot with intentional duplicates, verifies report generation and that hashes/ is preserved Tests auto-skip with clear message when prerequisites are missing. To run: ANTHROPIC_API_KEY=sk-... pytest -v -k Integration https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
…ompts - All three plugins now default to disabled (opt-in only) - Stricter system prompts: cleanup hook now explicitly requires writing cleanup_report.txt with full path, reinforced in both system prompt and user prompt - Extract hook system prompt stricter about saving files to output dir only - Added test_install_hook_skips_by_default for claudecode base plugin - Fixed integration test max_turns (haiku needs more turns for tool use) https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
… reorder - Fix iframe sandbox security: remove allow-same-origin + allow-scripts (all 3 plugins) - Switch env var filtering from allowlist to denylist approach in claudecode_utils - Add session.json logging as artifact in extract and cleanup output dirs - Move cleanup plugin from priority 99 to 92 (before hashes at 93) - Honor CLAUDECODE_BINARY env var in install hook - Create output dir after building system prompt (avoid self-listing) - Exclude response.txt/session.json from extract output file count - Fix docstring defaults to match code (ENABLED=false) - Strengthen error assertions in tests https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
- Remove all skipif gates from integration tests (always run in CI) - Fix max_turns test to compare cleanup default against extract default - Strengthen extract integration test to verify content.md generation - Add cleanup_report.txt assertion to test_cleanup_produces_report - Fix test_cleanup_preserves_hashes to test real deletion with hashes preserved - Remove unused shutil imports https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
…fy no-skip policy - Fix cleanup hook docstring referencing old priority 99 (now 92) - Add assertion verifying broken_extractor/ was actually deleted - Add docstring comments clarifying intentional lack of skip decorators https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
… naming - Include stderr detail in ArchiveResult error messages (not just exit code) - Exclude session.json from cleanup fallback output file count - Use basename for Binary record name when CLAUDECODE_BINARY is an absolute path - Tighten missing-binary assertions to require "not found" specifically - Clarify session log format comment in claudecode_utils.py - Document that CLAUDECODE_ENABLED must be set for child plugins to work https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
- Add CRITICAL RESTRICTION in system prompts: must not operate outside SNAP_DIR - Cleanup plugin: broaden allowed tools to full set (Read, Write, Edit, Bash, Glob, Grep) since it needs flexibility within the snapshot dir - Extract plugin: clarify read-only access to snapshot, write-only to output dir - Path scoping enforced via system prompt (Claude Code --allowedTools cannot restrict by path, only by command name) https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
Each README covers configuration env vars, binary dependencies, hook priorities, permissions/scope, output files, and usage examples. https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
New plugin that installs and drives the official Claude for Chrome extension (fcoeoabgfenejglbffodgkkbkcdhcgfn) to interact with pages during archiving. Hooks: - on_Crawl__84: Install extension from Chrome Web Store (like twocaptcha/ublock) - on_Crawl__96: Inject ANTHROPIC_API_KEY into extension storage after Chrome launch - on_Snapshot__47: Run user-configurable prompt on page via extension side panel (after infiniscroll@45, before singlefile@50) Features: - Configurable prompt via CLAUDECHROME_PROMPT env var - Downloads triggered by Claude moved from chrome_downloads/ to output dir - Conversation log saved as JSON + human-readable text - Default prompt: click all expand/show-more buttons Includes config.json, templates, tests (8 passing), and README. https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
Instead of trying to puppet the Claude for Chrome extension's side panel UI (fragile, requires OAuth login), the snapshot hook now directly implements the computer-use agentic loop: 1. Take screenshot via CDP Page.captureScreenshot 2. Send to Claude via Anthropic Messages API (computer_20250124 tool) 3. Execute actions (left_click, type, key, scroll, etc.) via puppeteer 4. Repeat until Claude responds text-only or max iterations reached Uses curl for API calls (reliable proxy support in all environments). Verified end-to-end: Claude successfully clicked a "Show More" button on a test page, revealed hidden content, and reported the secret message. Screenshots confirm page modification (19KB initial -> 28KB after click). Also adds CLAUDECHROME_MAX_ACTIONS config and fixes CHROME_SESSION_DIR path. https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…anup.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
….bg.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
- claudecode_utils.py: remove local get_env, get_env_bool, get_env_int, emit_archive_result; import from base.utils instead - install hook: remove local get_env, get_env_bool, output_binary; import from base.utils and use its output_binary with overrides param - claudecodeextract/claudecodecleanup: import get_env*, emit_archive_result directly from base.utils; keep build_system_prompt/run_claude_code from claudecode_utils - claudechrome snapshot hook: import emitArchiveResult from base/utils.js and replace inline JSON.stringify calls https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
…echrome integration test
- All 4 test files now import get_plugin_dir, get_hook_script,
parse_jsonl_output, run_hook from base.test_utils instead of
chrome_test_helpers (reducing coupling)
- Replace manual subprocess calls + inline JSONL parsing with
run_hook() and parse_jsonl_output() helpers
- claudechrome: replace placeholder test_full_pipeline_with_chrome_session
with real integration test using chrome_session context manager,
httpserver test page, and full output verification
- claudechrome: add test_snapshot_hook_fails_without_chrome_session
- Integration test class uses @pytest.mark.usefixtures("ensure_chrome_test_prereqs")
instead of module-level pytestmark so unit tests run without Chrome
https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
…ome sandbox/UID handling - Add ensure_claude_code_prereqs session fixture that validates claude binary in PATH and ANTHROPIC_API_KEY is set, with clear error messages - Override ensure_chromium_and_puppeteer_installed to scan well-known paths for pre-installed Chromium before falling back to hook-based install (fixes UID 1001 passwd entry issue in containers) - Auto-disable Chrome sandbox when running as root (CHROME_SANDBOX=false) - All 4 integration test classes now use @pytest.mark.usefixtures for their respective prereqs - All 46 tests pass (38 unit + 8 integration) https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
…er) works The ensure_chromium_and_puppeteer_installed fixture now goes straight through install_chromium_with_hooks with no manual path scanning. The NpmProvider UID 1001 fix is landing upstream separately. Only addition over upstream fixture: auto-disable Chrome sandbox when running as root (CHROME_SANDBOX=false). https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
findChromium() now checks LIB_DIR/chrome-linux/chrome and LIB_DIR/browsers/chrome/chrome before falling back to system locations. This lets _resolve_existing_chromium find Chromium installed by the hook-based install pipeline, avoiding redundant downloads from storage.googleapis.com. Also confirmed abx-pkg 0.6.4 UID/EUID fix resolves the NpmProvider passwd entry issue (pwd.getpwuid(1001)). All 46 tests pass (38 unit + 8 integration). https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
…LAUDECODE_BINARY, stop env propagation
- Move get_lib_dir() call before monkeypatch.setenv("HOME") so chrome_utils.js
sees the real home directory when resolving LIB_DIR
- Stop propagating NODE_MODULES_DIR/NODE_PATH/PATH from session fixture to
os.environ — tests must not depend on execution order
- Honor CLAUDECODE_BINARY env var in ensure_claude_code_prereqs fixture
- Replace ensure_claude_code_prereqs with ensure_anthropic_api_key on
claudechrome integration tests (claudechrome doesn't use Claude CLI)
- Add ensure_anthropic_api_key session fixture for plugins that call the
Anthropic API directly
https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
… fields - Rename claudecode and claudechrome install hooks from .bg. to .finite.bg. to match the new naming convention (finite bg hooks run once and terminate) - Add required metadata fields (title, description, required_plugins, required_binaries, output_mimetypes) to all four Claude plugin config.jsons - Update README references to use new hook filenames https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
7657a1b to
a2b0a17
Compare
Summary
This PR introduces three new ArchiveBox plugins that integrate Claude Code AI agents for intelligent content processing:
Key Changes
claudecode (Base Plugin)
@anthropic-ai/claude-codenpm packageget_env,get_env_bool,get_env_int)build_system_prompt(): Generates context-aware system prompts describing the ArchiveBox directory structure and available extractor outputsrun_claude_code(): Wrapper for spawning Claude Code CLI with configurable prompts, timeouts, models, and tool restrictionsemit_archive_result(): Helper for emitting ArchiveBox JSONL result recordsclaudecodeextract (Extraction Plugin)
CLAUDECODEEXTRACT_ENABLEDflag and requiresANTHROPIC_API_KEYclaudecodecleanup (Cleanup Plugin)
CLAUDECODECLEANUP_ENABLEDflag and requiresANTHROPIC_API_KEYTemplates and UI
Notable Implementation Details
Tool Restrictions: Claude Code is invoked with restricted tool access:
Configuration Inheritance: Child plugins (extract, cleanup) can override base claudecode settings (timeout, model, max_turns) via environment variables with fallback to base plugin defaults
System Prompt Context: The
build_system_prompt()function dynamically includes:Error Handling: All hooks gracefully handle missing API keys, missing binaries, and disabled states with appropriate JSONL result records
Comprehensive Testing: 253+ lines of tests covering hook existence, config validity, JSONL output format, environment variable handling, and error scenarios
https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
Summary by cubic
Adds
claudechromefor agentic page interaction via Chrome CDP and Anthropic computer-use, plusclaudecode,claudecodeextract, andclaudecodecleanuppowered by@anthropic-ai/claude-code. Adds abaseplugin for shared Python/JS helpers, standardizes JSONL output, tightens library permissions, and hardens CI with reliable Chromium and Anthropic prerequisites.New Features
claudechrome: optional install/config of the Claude for Chrome extension, but the snapshot hook now drives pages directly via Anthropic Messages (computer_20250124) usingcurl+puppeteer-core; runs at priority 47; loops screenshot→send→execute (click/type/scroll/key); moves downloads into the plugin output dir; saves a conversation log; addsCLAUDECHROME_MAX_ACTIONS; fixesCHROME_SESSION_DIR; templates hardened (iframe sandbox).claudecodesuite:claudecode: crawl install hook renamed to.finite.bg, installs theclaudeCLI (@anthropic-ai/claude-code), imports helpers frombase/utils, emits Binary JSONL, respectsCLAUDECODE_ENABLEDandCLAUDECODE_BINARY(uses basename if absolute).claudecodeextract(priority 58): reads prior outputs and writes derived content (defaultcontent.md) to its dir; scopes reads to SNAP_DIR and writes to its output dir; logssession.json; excludesresponse.txt/session.jsonfrom counts.claudecodecleanup(priority 92): dedupes within SNAP_DIR, writescleanup_report.txt, broadens allowed tools toRead,Write,Edit,Bash,Glob,Grep; tests verify real deletions and stricter error messages (include stderr).baseplugin: shared helpers inbase/utils.pyandbase/utils.js; refactors hooks/tests to use them; standardizes JSONL output; fixes stdlibsslshadowing viasys.path.append; adds icons; addsuv.lockfor reproducible deps.enforce_lib_permissions()makes~/.config/abx/lib/read‑exec only;findChromium()now searchesLIB_DIR; CI fixtures ensure Anthropic prerequisites, auto‑disable Chrome sandbox as root, and resolveLIB_DIRbeforeHOME.Migration
CLAUDECHROME_ENABLED=truewithANTHROPIC_API_KEY(requireschrome); setCLAUDECODE_ENABLED=truebefore enablingCLAUDECODEEXTRACT_ENABLED/CLAUDECODECLEANUP_ENABLED.claudeCLI is available (installed via npm or override withCLAUDECODE_BINARY).CLAUDECHROME_*(incl.CLAUDECHROME_MAX_ACTIONS),CLAUDECODE_*,CLAUDECODEEXTRACT_*, andCLAUDECODECLEANUP_*env vars.Written for commit a2b0a17. Summary will update on new commits.