Skip to content

Conversation

@delilahw
Copy link
Member

@delilahw delilahw commented Oct 31, 2025

The artifacts helper injects an access token before calling a shadowed tool (e.g. npx). This works by setting a shell function with the same name as the shadowed tool, which would take precedence over the tool and invoke the corresponding <shadowed tool>-run.sh script to inject the token and call the shadowed bin.

However, the above approach doesn't work in these situations:

  • When a shell is not used to invoke the shadowed tool. e.g. Popen([tool], shell=False) in Python.
  • When using a shell that we don't support, e.g. fish.
  • When a user overrides our shell config/dotfiles with their own.

For all of the above situations, we can use an executable wrapper instead of a shell function. Add the devcontainer option to set wrapperType == EXECUTABLE. When this option is selected, we should:

  1. Create a directory that takes precedence on the $PATH over the shadowed tool, by ordering our devcontainer feature's installation after each tool's feature. I.e., we must install our feature after"ghcr.io/devcontainers/features/node" because they modify $PATH in containerEnv as well.

  2. Create an executable in the above directory. When this executable is run, it should unwrap itself from the PATH and handover to the corresponding <shadowed tool>-run.sh script.

    If the executable appears more than once in the PATH, it should recursively unwrap itself until the shadowed tool is reached. The stack should look like this:

# Top-most call appears first

PATH=/usr/local/share/codespace-features:/usr/local/share/codespace-features:$PATH npx
⬇️
/usr/local/share/codespace-features/npx
⬇️
ARTIFACTS_HELPER_TARGET_BIN_PATHS="/usr/local/share/codespace-features/npx /path/to/real/npx"
NPX_EXE=/usr/local/share/codespace-features/npx
/usr/local/bin/run-npx.sh
⬇️
ARTIFACTS_HELPER_TARGET_BIN_PATHS="/usr/local/share/codespace-features/npx /path/to/real/npx"
/usr/local/share/codespace-features/npx
⬇️
ARTIFACTS_HELPER_TARGET_BIN_PATHS=""
NPX_EXE=/path/to/real/npx
/usr/local/bin/run-npx.sh
⬇️
/path/to/real/npx

# Most recent call appears last

Finally, add a set of tests to check that the executable wrappers work as expected. The dotnet wrapper is excluded because the injected environment variable differs from the others. This should be fine because the executable wrappers use the same template.

I haven't replicated the above "unwrap itself twice" scenario in the tests yet, so this PR is a draft. It could also use some polishing up, but this is the core idea behind the solution to #55.

Wrap the underlying tools with an executable script, rather than relying on shell functions or aliases. This means that these scenarios will now work:
  - Invoking `npx` from a JS script, e.g. during a pre-commit hook
  - Invoking
```py
popen(['npx'], shell=False)
```
WRAPPED_BINS_DIR=$(mktemp -d)
BASH_BIN_DIR=$(dirname "$(command -v bash)")
TEST_PATH="$WRAPPER_INSTALL_PATH:$WRAPPER_INSTALL_PATH:$WRAPPED_BINS_DIR:$BASH_BIN_DIR"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this actually makes the executable wrapper appear twice in which -a. Probably best to copy or symlink the wrappers to simulate that scenario. It's important to prevent infinite recursion for the wrapper and this would make for a pretty robust test in that regard.

@markphip markphip requested a review from Copilot October 31, 2025 12:18
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for an alternative "EXECUTABLE" wrapper type to the artifacts-helper feature, allowing package manager commands to be wrapped via executable scripts instead of shell functions. This enables authentication injection in scenarios where shell functions aren't recognized (e.g., Python subprocess calls with shell=False).

  • Introduces new wrapperType configuration option with values SHELL_FUNCTION (default) and EXECUTABLE
  • Implements executable wrapper template that recursively unwraps itself from PATH to call the underlying binary
  • Adds environment variable caching to avoid redundant which calls in run scripts

Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/artifacts-helper/devcontainer-feature.json Adds wrapperType option and PATH configuration for executable wrappers
src/artifacts-helper/install.sh Implements installation logic for both wrapper types, renaming variables from ALIASES_ARR to WRAPPED_BINS_ARR
src/artifacts-helper/scripts/executable-wrapper-template.sh New template script that recursively unwraps PATH to find and execute the real binary
src/artifacts-helper/scripts/run-*.sh Adds caching logic to reuse environment variables for executable paths
src/artifacts-helper/README.md Documents the new wrapperType option and wrapper type descriptions
test/artifacts-helper/scenarios.json Adds test scenario for executable wrapper type
test/artifacts-helper/scenario_executable_wrapper.sh Comprehensive test script validating wrapper behavior and token injection

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


if [ -z "$NEXT_SHADOWED_BIN" ]; then
log_line "Error: The real $TARGET_BIN_NAME could not be found on PATH."
log_line "which -a $TARGET_BIN_NAME:\n%s\n" "$(which -a $TARGET_BIN_NAME)"
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log_line function uses printf which expects format arguments to be separated, but here the format specifiers %s are embedded in the first argument string. The $(which -a $TARGET_BIN_NAME) expansion won't be properly substituted. This line should either use echo or properly separate the printf format string from its arguments.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +53
log_line "CC_SHADOWED_BINS:\n%s\n" "$ARTIFACTS_HELPER_TARGET_BIN_PATHS"
log_line "CURRENT_SCRIPT_PATH: %s\n" "$CURRENT_SCRIPT_PATH"
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as line 51: log_line calls printf with a single argument, so the format specifiers %s won't be replaced with the variable values in the second argument. These arguments will be ignored by printf.

Copilot uses AI. Check for mistakes.

| Wrapper Type | Description |
|-|-|
| `SHELL_FUNCTION` | Configures shell functions that wrap the commands to set the authentication token before calling the actual command. This is the default options. |
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'options' to 'option'.

Suggested change
| `SHELL_FUNCTION` | Configures shell functions that wrap the commands to set the authentication token before calling the actual command. This is the default options. |
| `SHELL_FUNCTION` | Configures shell functions that wrap the commands to set the authentication token before calling the actual command. This is the default option. |

Copilot uses AI. Check for mistakes.
@markphip
Copy link
Contributor

markphip commented Oct 31, 2025

How reliable/fragile is PATH? Maybe since the Codespaces environment is more structured and predicitable it would be fine, but I was originally hesitant to try something like this as I did not think we could trust PATH order.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants