Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/atuin-desktop-runtime/bindings/BlockErrorData.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Data for block error lifecycle event
*/
export type BlockErrorData = { message: string, };
3 changes: 3 additions & 0 deletions crates/atuin-desktop-runtime/bindings/BlockFinishedData.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Data for block finished lifecycle event
*/
export type BlockFinishedData = { exit_code: number | null, success: boolean, };
7 changes: 6 additions & 1 deletion crates/atuin-desktop-runtime/bindings/BlockLifecycleEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@
import type { BlockErrorData } from "./BlockErrorData";
import type { BlockFinishedData } from "./BlockFinishedData";

export type BlockLifecycleEvent = { "type": "started" } | { "type": "finished", "data": BlockFinishedData } | { "type": "cancelled" } | { "type": "error", "data": BlockErrorData };
/**
* Block lifecycle events
*
* Indicates state transitions during block execution.
*/
export type BlockLifecycleEvent = { "type": "started", "data": string } | { "type": "finished", "data": BlockFinishedData } | { "type": "cancelled" } | { "type": "error", "data": BlockErrorData } | { "type": "paused" };
5 changes: 5 additions & 0 deletions crates/atuin-desktop-runtime/bindings/ClientPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ import type { PromptIcon } from "./PromptIcon";
import type { PromptInput } from "./PromptInput";
import type { PromptOption } from "./PromptOption";

/**
* A prompt displayed to the user in the client application
*
* Prompts can include text input fields, dropdowns, and action buttons.
*/
export type ClientPrompt = { title: string, prompt: string, icon: PromptIcon | null, input: PromptInput | null, options: Array<PromptOption>, };
13 changes: 12 additions & 1 deletion crates/atuin-desktop-runtime/bindings/ClientPromptResult.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type ClientPromptResult = { button: string, value: string | null, };
/**
* The result from a client prompt interaction
*/
export type ClientPromptResult = {
/**
* The value of the button that was clicked
*/
button: string,
/**
* The value entered in an input field, if any
*/
value: string | null, };
11 changes: 9 additions & 2 deletions crates/atuin-desktop-runtime/bindings/DocumentBridgeMessage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BlockOutput } from "./BlockOutput";
import type { ClientPrompt } from "./ClientPrompt";
import type { ResolvedContext } from "./ResolvedContext";
import type { StreamingBlockOutput } from "./StreamingBlockOutput";
import type { JsonValue } from "./serde_json/JsonValue";

export type DocumentBridgeMessage = { "type": "blockContextUpdate", "data": { blockId: string, context: ResolvedContext, } } | { "type": "blockOutput", "data": { blockId: string, output: BlockOutput, } } | { "type": "clientPrompt", "data": { executionId: string, promptId: string, prompt: ClientPrompt, } };
/**
* Messages sent from the runtime to the client application
*
* These messages communicate execution state, output, and context updates
* to the desktop application frontend.
*/
export type DocumentBridgeMessage = { "type": "blockContextUpdate", "data": { blockId: string, context: ResolvedContext, } } | { "type": "blockStateChanged", "data": { blockId: string, state: JsonValue, } } | { "type": "blockExecutionOutputChanged", "data": { blockId: string, } } | { "type": "blockOutput", "data": { blockId: string, output: StreamingBlockOutput, } } | { "type": "clientPrompt", "data": { executionId: string, promptId: string, prompt: ClientPrompt, } };
3 changes: 3 additions & 0 deletions crates/atuin-desktop-runtime/bindings/ExecutionStatus.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Current status of block execution
*/
export type ExecutionStatus = { "type": "Running" } | { "type": "Success" } | { "type": "Failed", "data": string } | { "type": "Cancelled" };
7 changes: 5 additions & 2 deletions crates/atuin-desktop-runtime/bindings/GCEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import type { PtyMetadata } from "./PtyMetadata";

/**
* Grand Central Event - all events that can be emitted by the runtime
* Events emitted by the runtime for monitoring and telemetry
*
* These events provide visibility into runtime operations including block execution,
* SSH connections, PTY lifecycle, and runbook state changes.
*/
export type GCEvent = { "type": "ptyOpened", "data": PtyMetadata } | { "type": "ptyClosed", "data": { pty_id: string, } } | { "type": "blockStarted", "data": { block_id: string, runbook_id: string, } } | { "type": "blockFinished", "data": { block_id: string, runbook_id: string, success: boolean, } } | { "type": "blockFailed", "data": { block_id: string, runbook_id: string, error: string, } } | { "type": "blockCancelled", "data": { block_id: string, runbook_id: string, } } | { "type": "sshConnected", "data": { host: string, username: string | null, } } | { "type": "sshConnectionFailed", "data": { host: string, error: string, } } | { "type": "sshDisconnected", "data": { host: string, } } | { "type": "runbookStarted", "data": { runbook_id: string, } } | { "type": "runbookCompleted", "data": { runbook_id: string, } } | { "type": "runbookFailed", "data": { runbook_id: string, error: string, } };
export type GCEvent = { "type": "serialExecutionStarted", "data": { runbook_id: string, } } | { "type": "serialExecutionCompleted", "data": { runbook_id: string, } } | { "type": "serialExecutionCancelled", "data": { runbook_id: string, } } | { "type": "serialExecutionFailed", "data": { runbook_id: string, error: string, } } | { "type": "serialExecutionPaused", "data": { runbook_id: string, block_id: string, } } | { "type": "ptyOpened", "data": PtyMetadata } | { "type": "ptyClosed", "data": { pty_id: string, } } | { "type": "blockStarted", "data": { block_id: string, runbook_id: string, } } | { "type": "blockFinished", "data": { block_id: string, runbook_id: string, success: boolean, } } | { "type": "blockFailed", "data": { block_id: string, runbook_id: string, error: string, } } | { "type": "blockCancelled", "data": { block_id: string, runbook_id: string, } } | { "type": "sshConnected", "data": { host: string, username: string | null, } } | { "type": "sshConnectionFailed", "data": { host: string, error: string, } } | { "type": "sshDisconnected", "data": { host: string, } } | { "type": "sshCertificateLoadFailed", "data": { host: string, cert_path: string, error: string, } } | { "type": "sshCertificateExpired", "data": { host: string, cert_path: string, valid_until: string, } } | { "type": "sshCertificateNotYetValid", "data": { host: string, cert_path: string, valid_from: string, } } | { "type": "runbookStarted", "data": { runbook_id: string, } } | { "type": "runbookCompleted", "data": { runbook_id: string, } } | { "type": "runbookFailed", "data": { runbook_id: string, error: string, } };
3 changes: 3 additions & 0 deletions crates/atuin-desktop-runtime/bindings/PromptIcon.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Icon types for client prompts
*/
export type PromptIcon = { "type": "info" } | { "type": "warning" } | { "type": "error" } | { "type": "success" } | { "type": "question" };
3 changes: 3 additions & 0 deletions crates/atuin-desktop-runtime/bindings/PromptInput.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Input types for client prompts
*/
export type PromptInput = { "type": "string" } | { "type": "text" } | { "type": "dropdown", "data": Array<[string, string]> };
3 changes: 3 additions & 0 deletions crates/atuin-desktop-runtime/bindings/PromptOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
import type { PromptOptionColor } from "./PromptOptionColor";
import type { PromptOptionVariant } from "./PromptOptionVariant";

/**
* A button option in a client prompt dialog
*/
export type PromptOption = { label: string, value: string, variant: PromptOptionVariant | null, color: PromptOptionColor | null, };
3 changes: 3 additions & 0 deletions crates/atuin-desktop-runtime/bindings/PromptOptionColor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Color scheme for prompt options (buttons)
*/
export type PromptOptionColor = { "type": "default" } | { "type": "primary" } | { "type": "secondary" } | { "type": "success" } | { "type": "warning" } | { "type": "danger" };
3 changes: 3 additions & 0 deletions crates/atuin-desktop-runtime/bindings/PromptOptionVariant.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Visual variant for prompt options (buttons)
*/
export type PromptOptionVariant = { "type": "flat" } | { "type": "light" } | { "type": "shadow" } | { "type": "solid" } | { "type": "bordered" } | { "type": "faded" } | { "type": "ghost" };
21 changes: 20 additions & 1 deletion crates/atuin-desktop-runtime/bindings/PtyMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type PtyMetadata = { pid: string, runbook: string, block: string, created_at: bigint, };
/**
* Metadata about a PTY instance
*/
export type PtyMetadata = {
/**
* Unique PTY identifier
*/
pid: string,
/**
* Runbook ID this PTY belongs to
*/
runbook: string,
/**
* Block ID that created this PTY
*/
block: string,
/**
* Unix timestamp when PTY was created
*/
created_at: bigint, };
2 changes: 1 addition & 1 deletion crates/atuin-desktop-runtime/bindings/ResolvedContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
* Since it's built from a `ContextResolver`, it's a snapshot
* of the final context based on the blocks above it.
*/
export type ResolvedContext = { variables: { [key in string]?: string }, cwd: string, envVars: { [key in string]?: string }, sshHost: string | null, };
export type ResolvedContext = { variables: { [key in string]?: string }, variablesSources: { [key in string]?: string }, cwd: string, envVars: { [key in string]?: string }, sshHost: string | null, };
66 changes: 59 additions & 7 deletions crates/atuin-desktop-runtime/src/blocks/ssh_connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use crate::{
blocks::{Block, BlockBehavior, FromDocument},
client::LocalValueProvider,
context::{
BlockContext, ContextResolver, DocumentSshConfig, DocumentSshHost, SshIdentityKeyConfig,
BlockContext, ContextResolver, DocumentSshConfig, DocumentSshHost, SshCertificateConfig,
SshIdentityKeyConfig,
},
};
use async_trait::async_trait;
Expand Down Expand Up @@ -120,6 +121,38 @@ impl SshConnect {
}
}

/// Parse certificate configuration from local storage value
fn parse_certificate_from_local(value: &str) -> Option<SshCertificateConfig> {
// The value is JSON-encoded: {"mode": "...", "value": "..."}
let parsed: serde_json::Value = serde_json::from_str(value).ok()?;

let mode = parsed.get("mode").and_then(|v| v.as_str())?;
let cert_value = parsed.get("value").and_then(|v| v.as_str()).unwrap_or("");

match mode {
"none" | "" => Some(SshCertificateConfig::None),
"paste" => {
if cert_value.is_empty() {
None
} else {
Some(SshCertificateConfig::Paste {
content: cert_value.to_string(),
})
}
}
"path" => {
if cert_value.is_empty() {
None
} else {
Some(SshCertificateConfig::Path {
path: cert_value.to_string(),
})
}
}
_ => None,
}
}

/// Check if explicit settings are configured (user or hostname set)
pub fn has_explicit_config(&self) -> bool {
self.user.is_some() || self.hostname.is_some()
Expand Down Expand Up @@ -242,8 +275,8 @@ impl BlockBehavior for SshConnect {
return Err("Invalid SSH user_host format".into());
}

let identity_key = if let Some(provider) = block_local_value_provider {
match provider.get_block_local_value(self.id, "identityKey").await {
let (identity_key, certificate) = if let Some(provider) = block_local_value_provider {
let identity_key = match provider.get_block_local_value(self.id, "identityKey").await {
Ok(Some(value)) => {
tracing::debug!("Block {} read identityKey from KV: {}", self.id, value);
Self::parse_identity_key_from_local(&value)
Expand All @@ -256,15 +289,33 @@ impl BlockBehavior for SshConnect {
tracing::warn!("Failed to get identity key from local storage: {}", e);
None
}
}
};

let certificate = match provider.get_block_local_value(self.id, "certificate").await {
Ok(Some(value)) => {
tracing::debug!("Block {} read certificate from KV: {}", self.id, value);
Self::parse_certificate_from_local(&value)
}
Ok(None) => {
tracing::debug!("Block {} has no certificate in KV", self.id);
None
}
Err(e) => {
tracing::warn!("Failed to get certificate from local storage: {}", e);
None
}
};

(identity_key, certificate)
} else {
tracing::debug!("Block {} has no block_local_value_provider", self.id);
None
(None, None)
};
tracing::debug!(
"Block {} resolved identity_key to: {:?}",
"Block {} resolved identity_key to: {:?}, certificate to: {:?}",
self.id,
identity_key
identity_key,
certificate
);

// Backwards compatibility with older blocks that only check DocumentSshHost
Expand All @@ -276,6 +327,7 @@ impl BlockBehavior for SshConnect {
hostname: resolved_hostname,
port: self.port,
identity_key,
certificate,
});

Ok(Some(context))
Expand Down
49 changes: 47 additions & 2 deletions crates/atuin-desktop-runtime/src/blocks/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::execution::{
CancellationToken, ExecutionContext, ExecutionHandle, ExecutionStatus, StreamingBlockOutput,
};
use crate::pty::{Pty, PtyLike};
use crate::ssh::SshPty;
use crate::ssh::{SshPty, SshWarning};

/// Output structure for Terminal blocks that implements BlockExecutionOutput
/// for template access to terminal output.
Expand Down Expand Up @@ -303,7 +303,52 @@ impl Terminal {
}
};

let (pty_tx, resize_tx) = ssh_result?;
let (pty_tx, resize_tx, warnings) = ssh_result?;

// Emit events for any authentication warnings
for warning in warnings {
match warning {
SshWarning::CertificateLoadFailed {
host,
cert_path,
error,
} => {
let _ = context
.emit_gc_event(GCEvent::SshCertificateLoadFailed {
host,
cert_path,
error,
})
.await;
}
SshWarning::CertificateExpired {
host,
cert_path,
valid_until,
} => {
let _ = context
.emit_gc_event(GCEvent::SshCertificateExpired {
host,
cert_path,
valid_until,
})
.await;
}
SshWarning::CertificateNotYetValid {
host,
cert_path,
valid_from,
} => {
let _ = context
.emit_gc_event(GCEvent::SshCertificateNotYetValid {
host,
cert_path,
valid_from,
})
.await;
}
}
}

// Forward SSH output to binary channel and accumulate
let context_clone = context.clone();
Expand Down
14 changes: 14 additions & 0 deletions crates/atuin-desktop-runtime/src/context/block_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,18 @@ pub enum SshIdentityKeyConfig {
Path { path: String },
}

/// SSH certificate configuration from SSH Connect block
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "camelCase")]
pub enum SshCertificateConfig {
/// No custom certificate - auto-detect from key path or none
None,
/// Certificate content pasted directly
Paste { content: String },
/// Path to certificate file on local machine
Path { path: String },
}

/// Rich SSH configuration from SSH Connect block
///
/// This provides detailed SSH connection settings that override ~/.ssh/config.
Expand All @@ -410,6 +422,8 @@ pub struct DocumentSshConfig {
pub port: Option<u16>,
/// Identity key configuration
pub identity_key: Option<SshIdentityKeyConfig>,
/// SSH certificate configuration
pub certificate: Option<SshCertificateConfig>,
}

#[typetag::serde]
Expand Down
2 changes: 1 addition & 1 deletion crates/atuin-desktop-runtime/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub use block_context::BlockState;
pub use block_context::{
BlockContext, BlockContextItem, BlockExecutionOutput, BlockStateUpdater, BlockVars,
DocumentBlock, DocumentCwd, DocumentEnvVar, DocumentEnvVars, DocumentSshConfig,
DocumentSshHost, DocumentVar, DocumentVars, SshIdentityKeyConfig,
DocumentSshHost, DocumentVar, DocumentVars, SshCertificateConfig, SshIdentityKeyConfig,
};

pub use resolution::{ContextResolver, ResolvedContext};
Expand Down
24 changes: 24 additions & 0 deletions crates/atuin-desktop-runtime/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ pub enum GCEvent {
/// SSH connection closed
SshDisconnected { host: String },

/// SSH certificate file exists but failed to load (likely corrupted or invalid)
/// This is a warning - authentication will fall back to key-based auth
SshCertificateLoadFailed {
host: String,
cert_path: String,
error: String,
},

/// SSH certificate has expired
/// This is a warning - authentication fell back to key-based auth
SshCertificateExpired {
host: String,
cert_path: String,
valid_until: String,
},

/// SSH certificate is not yet valid
/// This is a warning - authentication fell back to key-based auth
SshCertificateNotYetValid {
host: String,
cert_path: String,
valid_from: String,
},

/// Runbook execution started
RunbookStarted { runbook_id: Uuid },

Expand Down
Loading
Loading