Skip to content

feat(v1.1.0): HTTP Client {{$keeper(...)}} integration + Run Keeper Securely run configuration#12

Merged
craiglurey merged 3 commits into
release/v1.1.0from
feature/run-config-and-http-client
May 7, 2026
Merged

feat(v1.1.0): HTTP Client {{$keeper(...)}} integration + Run Keeper Securely run configuration#12
craiglurey merged 3 commits into
release/v1.1.0from
feature/run-config-and-http-client

Conversation

@pkamble-ks
Copy link
Copy Markdown

Summary

Adds two major features for v1.1.0 and refactors the existing "Run Keeper Securely" pipeline into a shared runner.

  • Run Keeper Securely as a saved Run Configuration (Run → Edit Configurations → + → Keeper Securely) — configurable .env path, working directory, command, with output streamed to the Run tool window. Defaults are auto-detected for Python SDK / venv and common entry scripts (main.py, app.py, manage.py) when a new configuration is created.
  • JetBrains HTTP Client integration for .http / .rest files via the dynamic variable {{ $keeper("recordUid", "field") }}. The existing Get Keeper Secret action now detects file type and inserts the correct snippet (keeper://... for .env/scripts, {{$keeper(...)}} for HTTP Client files).
  • Refactor: extracted a shared KeeperSecureScriptRunner pipeline used by both the Tools menu action and the new run configuration. Slims KeeperSecretAction.kt by ~500 lines.

Closes #9, #11.

Changes

New — Run Configuration (keepersecurity.run)

  • KeeperSecureRunConfiguration.java — run configuration implementation
  • KeeperSecureRunConfigurationType.kt — registers the configuration type
  • KeeperSecureRunConfigurationEditor.kt — settings UI
  • KeeperSecureRunConfigurationOptions.kt — persisted options
  • KeeperSecureRunProfileState.kt — execution state
  • KeeperSecureProcessHandler.kt — process lifecycle / output streaming
  • KeeperSecureScriptRunner.kt — shared pipeline (env resolution + execution)
  • KeeperSecureRunDefaults.kt — Python SDK / venv / entry-script auto-detection
  • KeeperSecurePathUtil.kt — path resolution helpers

New — HTTP Client integration (keepersecurity.http)

  • KeeperHttpClientKeeperFunctionValue.java — implements the $keeper(...) function
  • KeeperHttpClientDynamicVariablesProvider.kt — registers the dynamic variable
  • KeeperHttpSecretResolver.kt — resolves recordUid/field against the vault
  • META-INF/keeper-http-client.xml — optional plugin descriptor (loaded only when com.jetbrains.restClient is present)

Updated

  • action/KeeperGetSecretAction.kt — file-type-aware snippet insertion
  • action/KeeperSecretAction.kt — delegates to KeeperSecureScriptRunner
  • util/KeeperRecordFieldExtractor.kt — shared field extraction utility
  • META-INF/plugin.xml — registers run configuration type + optional com.jetbrains.restClient dependency

Build

  • gradle.propertiespluginVersion 1.0.0 → 1.1.0, platformType IC → IU, adds platformBundledPlugins=com.jetbrains.restClient (required to compile against com.intellij.httpClient.*)
  • build.gradle.kts — JDK 21 toolchain note

Docs & assets

  • CHANGELOG.md — 1.1.0 entry
  • README.md — HTTP Client + run configuration sections, prerequisites
  • META-INF/pluginIcon.svg / pluginIcon_dark.svg

Compatibility notes

  • The com.jetbrains.restClient dependency is declared optional in plugin.xml, so the plugin still loads on IDEs without HTTP Client (e.g. IntelliJ IDEA Community). HTTP-Client-specific features simply won't register in those IDEs.
  • Build now targets platformType=IU because the HTTP Client APIs are only bundled with Ultimate-tier IDEs. Community-only verification can be done by overriding platformType=IC via Gradle property.
  • Requires Gradle on JDK 21 (Eclipse Temurin or JetBrains Runtime). Set via IDE → Settings → Build → Gradle → Gradle JVM.

Test plan

  • ./gradlew clean build succeeds on JDK 21
  • ./gradlew runIde launches a sandbox IDE with the plugin loaded
  • Run Configuration
    • Run → Edit Configurations → + → Keeper Securely creates a new config
    • Defaults auto-fill Python SDK / venv when a Python project is detected
    • Running the config resolves keeper:// references in .env and streams script output to the Run tool window
    • Stopping the run terminates the child process cleanly
  • HTTP Client integration (Ultimate / WebStorm / etc.)
    • In a .http file, {{ $keeper("<recordUid>", "<field>") }} resolves to the live vault value at request time
    • Tools → Keeper Vault → Get Keeper Secret inside a .http file inserts {{$keeper(...)}}
    • Same action inside a .env / script file inserts keeper://...
  • Backward compatibility
    • All existing Tools-menu actions (Auth, Add/Update/Get Record, Generate, Folder Select) still work
    • Plugin loads on IntelliJ IDEA Community (HTTP Client features are absent but no errors)
  • ./gradlew verifyPlugin passes

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

Qodana Community for JVM

It seems all right 👌

No new problems were found according to the checks applied

💡 Qodana analysis was run in the pull request mode: only the changed files were checked

View the detailed Qodana report

To be able to view the detailed Qodana report, you can either:

To get *.log files or any other Qodana artifacts, run the action with upload-result option set to true,
so that the action will upload the files as the job artifacts:

      - name: 'Qodana Scan'
        uses: JetBrains/qodana-action@v2025.1.1
        with:
          upload-result: true
Contact Qodana team

Contact us at qodana-support@jetbrains.com

- Remove unused imports (WriteCommandAction, ProgressManager, ExperimentalSerializationApi) and redundant @OptIn
- Drop redundant casts on RunConfiguration#getOptions(); switch to property-access syntax (s.options, cfg.options, File#isFile)
- Drop unnecessary safe calls / .trim() on non-null Strings in KeeperSecureRunConfigurationEditor
- Convert HTTP Client snippet builder to a string template
- Sentence-case the background task title ("Run Keeper securely")
- Suppress UNUSED_PARAMETER on the editor's project ctor arg (kept to satisfy the Java caller's contract)

No behavior change. Compile + lints clean.
@pkamble-ks pkamble-ks requested a review from stas-schaller May 6, 2026 07:08
@craiglurey craiglurey merged commit eeedd87 into release/v1.1.0 May 7, 2026
9 checks passed
trimmedField,
logger,
)
extracted ?: "[Keeper] Field '$trimmedField' not found in record $trimmedUid"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This potentially sends sensitive field and UID information to the remote server on failure. Instead of returning an error string as the variable value, throw so the HTTP Client surfaces it as an error and the request doesn't fire. Something like
extracted ?: throw IllegalStateException("Keeper field '$trimmedField' not found in record $trimmedUid")

extracted ?: "[Keeper] Field '$trimmedField' not found in record $trimmedUid"
} catch (e: Exception) {
logger.warn("Keeper HTTP variable resolution failed: ${e.message}", e)
"[Keeper] ${e.message ?: e::class.simpleName}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same issue as line 38 above. I recommend throwing an error rather than logging to the server:

} catch (e: Exception) {
    logger.warn("Keeper HTTP variable resolution failed: ${e.message}", e)
    throw e
}

}.queue()
}

override fun destroyProcessImpl() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With this empty, when users press the stop button nothing will happen and the user will likely be confused. Consider implementing destroyProcessImpl with a process spawned in the ScriptRunner and passed to this Handler via an onProcessStarted callback. Store the reference as @Volatile since destroyProcessImpl and startNotify run on different threads.

override fun getProcessInput(): java.io.OutputStream? = null

override fun startNotify() {
super.startNotify()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

startNotify() should signal that an already-running process is ready for output, not launch the work. The process should be started before startNotify() is called, with destroyProcessImpl() holding a reference to it. As written, the run lifecycle timings are misaligned with actual execution and Stop cannot be wired up cleanly.

logger.info("Found ${keeperRefs.size} Keeper references to process")
indicator.text = "Fetching ${keeperRefs.size} secrets via persistent shell..."

keeperRefs.forEachIndexed { index, (key, uid, field) ->
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Cancellation is never checked in this loop. Add if (indicator.isCanceled) throw ProcessCanceledException() at the top of each iteration so Cancel is honored between secret fetches.


override fun applyEditorTo(s: KeeperSecureRunConfiguration) {
val o = s.options
val env = envFileField.text.trim().ifBlank { ".env" }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ifBlank { ".env" } silently overwrites a blank field when the user saves. If they cleared it intentionally, checkConfiguration will still error. Let it do that directly rather than hiding the blank input. Same issue on line 54 in resetEditorFrom.


private fun isHttpClientRequestFile(file: VirtualFile): Boolean {
val ext = file.extension ?: return false
return ext.equals("http", ignoreCase = true) || ext.equals("rest", ignoreCase = true)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

File type detected by extension string. Consider using the FileType API instead. Compare file.fileType against HttpRequestFileType.INSTANCE (guarded by the optional com.jetbrains.restClient dependency check) to handle custom associations and scratch files correctly.

}

private fun buildInsertionSuccessMessage(insertText: String, keeperNotation: String): String {
val isHttpSnippet = insertText.startsWith("{{") && insertText.contains("keeper(")
Copy link
Copy Markdown
Collaborator

@stas-schaller stas-schaller May 7, 2026

Choose a reason for hiding this comment

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

Detecting the snippet type by pattern-matching the output string is fragile, it breaks if the snippet format changes. Consider passing a Boolean from insertionTextForCurrentFile into buildInsertionSuccessMessage instead.

Comment thread CHANGELOG.md
@@ -1,5 +1,15 @@
# Keeper Security JetBrains Plugin Changelog

## [Unreleased]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pin the version number here instead of Unreleased

Comment thread gradle.properties

pluginGroup = com.keepersecurity.jetbrains
pluginName = Keeper Security
pluginRepositoryUrl = https://github.com/[your-username]/keeper-jetbrains-plugin
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Placeholder URL. Update to https://github.com/Keeper-Security/keeper-jetbrains-plugin before publishing

pkamble-ks added a commit that referenced this pull request May 8, 2026
Throw on HTTP variable resolution failures instead of returning error strings (avoids leaking record UID / field path into outbound requests). Wire the run-config Stop button via @volatile Process + ProgressIndicator references and honor cancellation between secret fetches. Drain stdout on a separate thread and bound process.waitFor with a periodic poll so hung scripts can be force-killed.

Use the FileType API (guarded by a com.jetbrains.restClient plugin check) to detect HTTP Client request files; pass an explicit Boolean into the success-message builder instead of pattern-matching the inserted text.

Drop the silent ".env" substitution from the run-config editor, drop the duplicate applyDefaults call, and remove the unconditional 5s sleep after the Keeper shell starts in favor of a poll-until-prompt loop. Tighten the readiness check to a liveness probe plus the authoritative KeeperShellService.isReady() flag.

Pin CHANGELOG to 1.1.0 and replace the placeholder pluginRepositoryUrl.
@pkamble-ks
Copy link
Copy Markdown
Author

Hi @stas-schaller — thanks for the thorough review on this. All 14 items are addressed in #13. Two callouts where the implementation deviates slightly from the literal suggestion:

  • feat(v1.1.0): HTTP Client {{$keeper(...)}} integration + Run Keeper Securely run configuration #12 (comment) (startNotify lifecycle): kept the work in startNotify and wired Stop via @Volatile Process + ProgressIndicator references (the structural fix from the destroyProcessImpl comment). Doing the strict "process-before-startNotify" reorder would mean fetching secrets synchronously in RunProfileState.execute(), blocking the EDT. Verified end-to-end in the sandbox: clicking Stop during the user command terminates within ~1 s and surfaces "Cancelled by user". Happy to refactor further if you'd prefer the strict lifecycle.

  • feat(v1.1.0): HTTP Client {{$keeper(...)}} integration + Run Keeper Securely run configuration #12 (comment) (positive prompt matches in isReady): I tried this first and it broke the authenticated path. KeeperShellService.executeCommand runs every response through extractCommandOutput, which strips My Vault>, Keeper>, and Not logged in> from the result before returning, so positive content matching can't ever hit on a healthy shell. Switched to a liveness-only probe plus KeeperShellService.isReady() (which is set during startup once a real prompt is observed on the raw output). The negative-only fallback you flagged is gone.

Tagging you on #13 for review.

stas-schaller added a commit that referenced this pull request May 12, 2026
…tp-client

Address PR #12 review feedback (1.1.0 follow-up)
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.

3 participants