Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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, };
53 changes: 52 additions & 1 deletion crates/atuin-desktop-runtime/src/blocks/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ use uuid::Uuid;

use crate::blocks::{Block, BlockBehavior};
use crate::context::{fs_var, BlockExecutionOutput, BlockVars};
use crate::events::GCEvent;
use crate::execution::{
CancellationToken, ExecutionContext, ExecutionHandle, ExecutionStatus, StreamingBlockOutput,
};
use crate::ssh::OutputLine as SessionOutputLine;
use crate::ssh::{OutputLine as SessionOutputLine, SshWarning};

use super::FromDocument;

Expand Down Expand Up @@ -705,6 +706,7 @@ impl Script {
let channel_id = self.id.to_string();
let (output_sender, mut output_receiver) = mpsc::channel::<SessionOutputLine>(100);
let (result_tx, result_rx) = oneshot::channel::<()>();
let (warnings_tx, warnings_rx) = oneshot::channel::<Vec<SshWarning>>();

let captured_output = Arc::new(RwLock::new(Vec::new()));
let captured_output_clone = captured_output.clone();
Expand Down Expand Up @@ -733,6 +735,7 @@ impl Script {
output_sender,
result_tx,
ssh_config,
Some(warnings_tx),
) => {
result
}
Expand All @@ -757,6 +760,54 @@ impl Script {
}
return (Err(error_msg.into()), Vec::new(), None);
}

// Receive and emit SSH authentication warnings (certificate issues, etc.)
if let Ok(warnings) = warnings_rx.await {
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;
}
}
}
}

let context_clone = context.clone();
let block_id = self.id;
let ssh_pool_clone = ssh_pool.clone();
Expand Down
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
Loading
Loading