From 1b0572b3810be6566e5c235e3f98394957226d03 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 3 Jul 2026 23:01:51 -0400 Subject: [PATCH 1/9] feat(operator): auto-register Telegram webhook, aws-sm:// secret refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related additions to `spec.secrets`/ingress handling: 1. `apply` now calls Telegram's `setWebhook` API automatically when `/webhook/telegram` is one of `spec.ingress.paths` and `spec.secrets` has a `TELEGRAM_BOT_TOKEN` entry — no manual `curl setWebhook` step needed after deploying a Telegram bot. If `TELEGRAM_SECRET_TOKEN` is also set, it's passed through so Telegram signs webhook requests with it. Best-effort: a failure here (bad token, network blip) is printed as a warning but never fails `apply` — the AWS provisioning already succeeded by that point. LINE has no equivalent API, so its webhook URL still needs manual registration. 2. `spec.secrets` values now accept the same `aws-sm://#` shorthand openab itself uses for in-app secret refs in config.toml, alongside the existing ECS-native `valueFrom` format (a full ARN, optionally with a `:::` suffix). ECS has no knowledge of the `aws-sm://` scheme, so it's translated to the ECS-native form before being passed to `register_task_definition` — a bare secret name is resolved to its ARN via `DescribeSecret` first, since ECS's `valueFrom` requires a full ARN whenever a JSON-key suffix is present. The two are related: registering the webhook needs the actual plaintext token value (not just an ECS valueFrom reference), so a new `operator/src/secrets.rs` module centralizes both resolution paths (`resolve_value_from` for ECS, `resolve_string` for in-process use) on top of one shared `aws-sm://` URI parser, rather than duplicating it. Adds `reqwest` (rustls-tls, no default features) as a new dependency — the only way to call Telegram's HTTP API from oabctl. Testing: cargo build/clippy --all-targets -D warnings/test all clean on macmini; 26/26 tests pass (7 new: aws-sm:// URI parsing edge cases, find_telegram_webhook path/secret gating logic). --- operator/Cargo.toml | 1 + operator/README.md | 41 +++++++++++-- operator/src/apply.rs | 36 +++++++++--- operator/src/ingress.rs | 113 ++++++++++++++++++++++++++++++++++++ operator/src/main.rs | 1 + operator/src/secrets.rs | 125 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 304 insertions(+), 13 deletions(-) create mode 100644 operator/src/secrets.rs diff --git a/operator/Cargo.toml b/operator/Cargo.toml index fea5b37ff..dd0f04892 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -36,3 +36,4 @@ toml = "0.8" anyhow = "1.0" dirs = "6" rpassword = "7" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } diff --git a/operator/README.md b/operator/README.md index 0e4fbb0d9..b139ae8b0 100644 --- a/operator/README.md +++ b/operator/README.md @@ -251,8 +251,7 @@ On `apply` this reconciles (idempotently, reused by name): 5. One **route** per path + a `prod` auto-deploy **stage** 6. A self-referencing **security-group** inbound rule on `containerPort` -`apply` then prints the stable webhook URL(s) to register with BotFather / the -LINE console: +`apply` then prints the stable webhook URL(s): ``` 🔗 Webhook URL(s) for my-bot: @@ -260,15 +259,27 @@ LINE console: https://{api-id}.execute-api.{region}.amazonaws.com/prod/webhook/line ``` +For Telegram, if `/webhook/telegram` is one of `spec.ingress.paths` and +`spec.secrets` has a `TELEGRAM_BOT_TOKEN` entry, `apply` also registers the +webhook URL with Telegram directly (calling `setWebhook` on your behalf) — no +manual `curl` step needed. If `spec.secrets` also has a +`TELEGRAM_SECRET_TOKEN` entry, it's passed through so Telegram signs every +webhook request with it (see the security note below). This is best-effort +and never fails `apply` — if it errors (bad token, network blip), `apply` +still succeeds and prints a warning; register the webhook yourself with the +printed URL in that case. LINE has no `setWebhook`-equivalent API, so its URL +must still be registered manually in the LINE Developers console. + > **Security note:** the API Gateway endpoint itself is public and unauthenticated > at the transport layer (no IAM auth, no API key). OpenAB's webhook handlers add > their own app-layer verification on top: Telegram validates the > `X-Telegram-Bot-Api-Secret-Token` header (`TELEGRAM_SECRET_TOKEN`) and the > request's source IP against Telegram's published webhook subnets; LINE > verifies an HMAC-SHA256 signature using `LINE_CHANNEL_SECRET`. Set -> `TELEGRAM_SECRET_TOKEN` when registering the webhook with BotFather to enable -> that check. +> `TELEGRAM_SECRET_TOKEN` in `spec.secrets` to enable that check — `apply` +> passes it to Telegram automatically as described above. > + > **Stage prefix stripped before it reaches the backend:** for private > (VPC_LINK) integrations, API Gateway forwards the *stage-prefixed* request > path to the backend by default (e.g. `/prod/webhook/telegram`, not @@ -395,6 +406,28 @@ Attached to `oab-task-role` — the identity the *running container* assumes. | `oab-s3-artifacts` | s3:GetObject, s3:PutObject | `{bucket}/artifacts/*` | | `oab-secrets` | secretsmanager:GetSecretValue | `arn:aws:secretsmanager:*:*:secret:oab/*` | +### `spec.secrets` value format + +`spec.secrets` maps a container env var name to where ECS should fetch its +value from at task launch (via the execution role below — the container +itself never sees a raw secret reference, only the resolved value). Two +formats are accepted, and can be mixed within the same manifest: + +```yaml +spec: + secrets: + # ECS-native valueFrom: a full Secrets Manager ARN. Add a `:::` + # suffix to extract one field of a JSON secret; omit it for a + # plain-string secret. + TELEGRAM_BOT_TOKEN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:oab/telegram/mybot-AC80TP:TELEGRAM_BOT_TOKEN::" + # aws-sm://# shorthand — the same convention + # openab itself uses for in-app secret refs in config.toml (see + # docs/secrets-management.md). oabctl resolves this to the ECS-native + # form above automatically; can be a bare secret name + # (resolved to its ARN via DescribeSecret) or a full ARN directly. + TELEGRAM_SECRET_TOKEN: "aws-sm://oab/telegram/mybot#TELEGRAM_SECRET_TOKEN" +``` + ### IAM Execution Role Permissions Attached to `oab-task-execution` — the identity **ECS itself** assumes to pull diff --git a/operator/src/apply.rs b/operator/src/apply.rs index 45566a7b9..5b2f6409c 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -255,15 +255,18 @@ async fn apply_ecs( env_vars.push(KeyValuePair::builder().name("BOOTSTRAP_FROM").value(bootstrap).build()); } - // 3. Build secrets from map - let secrets: Vec = m - .spec - .secrets - .iter() - .map(|(name, ssm_path)| { - Secret::builder().name(name).value_from(ssm_path).build().unwrap() - }) - .collect(); + // 3. Build secrets from map. Values can be either the ECS-native + // `valueFrom` format directly (a Secrets Manager ARN, optionally with + // a `:::` suffix), or the same `aws-sm://#` + // shorthand openab itself uses for in-app secret refs — resolved here + // into the ECS-native form ECS actually requires, since ECS has no + // knowledge of that scheme. + let sm = aws_sdk_secretsmanager::Client::new(config); + let mut secrets: Vec = Vec::with_capacity(m.spec.secrets.len()); + for (name, value) in &m.spec.secrets { + let value_from = crate::secrets::resolve_value_from(&sm, value).await?; + secrets.push(Secret::builder().name(name).value_from(value_from).build().unwrap()); + } // 4. Register task definition. Resolve bootstrap state once up front — it // supplies both the CloudWatch log group (for logConfiguration below) and @@ -555,6 +558,21 @@ async fn apply_ecs( for u in &urls { println!(" {u}"); } + + // Best-effort: register the Telegram webhook so the bot starts + // receiving updates without a manual `curl setWebhook` step. Only + // fires when `/webhook/telegram` is one of the ingress paths and + // spec.secrets has a TELEGRAM_BOT_TOKEN entry; a no-op otherwise. + // Never fails `apply` — the AWS provisioning above already + // succeeded, and this is a convenience on top of it. + let path_urls: Vec<(String, String)> = + ingress.paths.iter().cloned().zip(urls.iter().cloned()).collect(); + match crate::ingress::register_telegram_webhook(config, &m.spec.secrets, &path_urls).await + { + Ok(Some(desc)) => eprintln!(" ✓ Telegram webhook registered: {desc}"), + Ok(None) => {} + Err(e) => eprintln!(" ⚠ Telegram webhook registration failed (apply still succeeded): {e}"), + } } if wait { diff --git a/operator/src/ingress.rs b/operator/src/ingress.rs index 3c33d7db9..6545e24e1 100644 --- a/operator/src/ingress.rs +++ b/operator/src/ingress.rs @@ -139,6 +139,90 @@ pub async fn ensure_gateway( Ok(webhook_urls(&api_endpoint, &ingress.paths)) } +/// Find the `/webhook/telegram` URL among the resolved webhook URLs, and +/// confirm a `TELEGRAM_BOT_TOKEN` secret is configured. Returns `None` if +/// either is missing, meaning [`register_telegram_webhook`] should no-op. +fn find_telegram_webhook( + secrets: &std::collections::HashMap, + webhook_urls: &[(String, String)], +) -> Option<(String, String)> { + let url = webhook_urls + .iter() + .find(|(path, _)| path == "/webhook/telegram") + .map(|(_, url)| url.clone())?; + let token_ref = secrets.get("TELEGRAM_BOT_TOKEN")?.clone(); + Some((url, token_ref)) +} + +/// Register the webhook URL with Telegram's Bot API (`setWebhook`), so the +/// bot starts receiving updates without a manual `curl` step. Only runs when +/// `spec.secrets` has a `TELEGRAM_BOT_TOKEN` entry and one of the ingress +/// paths is `/webhook/telegram`; a no-op otherwise. If `TELEGRAM_SECRET_TOKEN` +/// is also present, it's passed through so Telegram includes it on every +/// webhook request (openab's Telegram adapter validates it). +/// +/// Best-effort: errors are returned to the caller to print as a warning, but +/// are never fatal to `apply` — the AWS-side provisioning already succeeded +/// by this point, and a failed Telegram API call (e.g. bad token, network +/// blip) shouldn't roll any of that back or fail the whole command. +pub async fn register_telegram_webhook( + config: &aws_config::SdkConfig, + secrets: &std::collections::HashMap, + webhook_urls: &[(String, String)], +) -> Result> { + let Some((url, token_arn)) = find_telegram_webhook(secrets, webhook_urls) else { + return Ok(None); + }; + + let sm = aws_sdk_secretsmanager::Client::new(config); + let bot_token = crate::secrets::resolve_string(&sm, &token_arn) + .await + .context("failed to resolve TELEGRAM_BOT_TOKEN")?; + + let secret_token = match secrets.get("TELEGRAM_SECRET_TOKEN") { + Some(v) => Some( + crate::secrets::resolve_string(&sm, v) + .await + .context("failed to resolve TELEGRAM_SECRET_TOKEN")?, + ), + None => None, + }; + + let mut form = vec![("url".to_string(), url)]; + if let Some(st) = secret_token { + form.push(("secret_token".to_string(), st)); + } + + let client = reqwest::Client::new(); + let resp = client + .post(format!("https://api.telegram.org/bot{bot_token}/setWebhook")) + .form(&form) + .send() + .await + .context("failed to call Telegram setWebhook API")?; + let status = resp.status(); + let body: serde_json::Value = resp + .json() + .await + .context("failed to parse Telegram setWebhook response")?; + + if !status.is_success() || body.get("ok").and_then(|v| v.as_bool()) != Some(true) { + anyhow::bail!( + "Telegram setWebhook failed: {}", + body.get("description") + .and_then(|v| v.as_str()) + .unwrap_or("unknown error") + ); + } + Ok(Some( + body.get("description") + .and_then(|v| v.as_str()) + .unwrap_or("webhook registered") + .to_string(), + )) +} + + /// API Gateway route key for a webhook path (POST only). fn route_key(path: &str) -> String { format!("POST {path}") @@ -945,6 +1029,35 @@ mod tests { assert!(has_stage_path_override(Some(¶ms))); } + #[test] + fn find_telegram_webhook_finds_url_and_token() { + let secrets = HashMap::from([("TELEGRAM_BOT_TOKEN".to_string(), "arn:aws:...".to_string())]); + let urls = vec![ + ("/webhook/line".to_string(), "https://x/prod/webhook/line".to_string()), + ("/webhook/telegram".to_string(), "https://x/prod/webhook/telegram".to_string()), + ]; + let (url, token) = find_telegram_webhook(&secrets, &urls).unwrap(); + assert_eq!(url, "https://x/prod/webhook/telegram"); + assert_eq!(token, "arn:aws:..."); + } + + #[test] + fn find_telegram_webhook_none_without_telegram_path() { + let secrets = HashMap::from([("TELEGRAM_BOT_TOKEN".to_string(), "arn:aws:...".to_string())]); + let urls = vec![("/webhook/line".to_string(), "https://x/prod/webhook/line".to_string())]; + assert!(find_telegram_webhook(&secrets, &urls).is_none()); + } + + #[test] + fn find_telegram_webhook_none_without_bot_token_secret() { + let secrets = HashMap::new(); + let urls = vec![( + "/webhook/telegram".to_string(), + "https://x/prod/webhook/telegram".to_string(), + )]; + assert!(find_telegram_webhook(&secrets, &urls).is_none()); + } + #[test] fn cloud_map_service_id_parses_from_arn() { assert_eq!( diff --git a/operator/src/main.rs b/operator/src/main.rs index 93ba84fcb..6c3aa3700 100644 --- a/operator/src/main.rs +++ b/operator/src/main.rs @@ -6,6 +6,7 @@ mod create; mod get; mod delete; mod ingress; +mod secrets; use clap::{Parser, Subcommand}; diff --git a/operator/src/secrets.rs b/operator/src/secrets.rs new file mode 100644 index 000000000..d284f60d9 --- /dev/null +++ b/operator/src/secrets.rs @@ -0,0 +1,125 @@ +//! Shared resolution for `spec.secrets` values. +//! +//! Values can be either a Secrets Manager reference in ECS-native +//! `valueFrom` format directly (a full ARN, optionally suffixed with +//! `:::` to extract one field of a JSON secret), or the same +//! `aws-sm://#` shorthand openab itself uses for +//! in-app secret refs (see `crates/openab-core/src/secrets.rs`) — kept +//! identical here so a manifest author can write one convention across both +//! `spec.secrets` (consumed by ECS at container launch) and `config.toml` +//! (consumed by openab itself at runtime). + +use anyhow::{Context, Result}; + +/// Parse `aws-sm://#` into `(secret_id, json_key)`. +/// Returns `None` if `value` doesn't use the `aws-sm://` scheme. +fn parse_aws_sm_uri(value: &str) -> Option> { + let rest = value.strip_prefix("aws-sm://")?; + Some(match rest.rsplit_once('#') { + Some((secret_id, json_key)) if !secret_id.is_empty() && !json_key.is_empty() => { + Ok((secret_id, json_key)) + } + _ => Err(anyhow::anyhow!( + "invalid aws-sm:// secret ref '{value}' — expected aws-sm://#" + )), + }) +} + +/// Resolve a `spec.secrets` value into the ECS-native `valueFrom` format ECS +/// actually requires. ECS's `valueFrom` requires the *full* ARN (not just a +/// secret name) whenever a JSON-key suffix is present, so an `aws-sm://` +/// secret-id that isn't already an ARN is resolved to its ARN via +/// `DescribeSecret` first. Values already in ECS-native format are passed +/// through unchanged. +pub async fn resolve_value_from( + sm: &aws_sdk_secretsmanager::Client, + value: &str, +) -> Result { + let Some(parsed) = parse_aws_sm_uri(value) else { + return Ok(value.to_string()); + }; + let (secret_id, json_key) = parsed?; + + let arn = if secret_id.starts_with("arn:") { + secret_id.to_string() + } else { + sm.describe_secret() + .secret_id(secret_id) + .send() + .await + .with_context(|| format!("failed to resolve secret '{secret_id}' to an ARN"))? + .arn() + .with_context(|| format!("secret '{secret_id}' has no ARN"))? + .to_string() + }; + Ok(format!("{arn}:{json_key}::")) +} + +/// Resolve a `spec.secrets` value to its plain string content, for callers +/// that need the actual secret value in-process (e.g. calling a third-party +/// API on the caller's behalf) rather than an ECS `valueFrom` reference. +/// Supports the same two forms as [`resolve_value_from`]: `aws-sm://...#...` +/// (fetched and JSON-key-extracted here), or a plain/ECS-native Secrets +/// Manager ARN — including one already carrying a `:::` suffix, +/// which `GetSecretValue` resolves natively. +pub async fn resolve_string(sm: &aws_sdk_secretsmanager::Client, value: &str) -> Result { + if let Some(parsed) = parse_aws_sm_uri(value) { + let (secret_id, json_key) = parsed?; + let secret_string = sm + .get_secret_value() + .secret_id(secret_id) + .send() + .await + .with_context(|| format!("failed to fetch secret '{secret_id}' from Secrets Manager"))? + .secret_string() + .with_context(|| format!("secret '{secret_id}' has no string value"))? + .to_string(); + let json: serde_json::Value = serde_json::from_str(&secret_string) + .with_context(|| format!("secret '{secret_id}' is not valid JSON"))?; + return json + .get(json_key) + .and_then(|v| v.as_str()) + .map(|v| v.to_string()) + .with_context(|| format!("JSON key '{json_key}' not found in secret '{secret_id}'")); + } + + sm.get_secret_value() + .secret_id(value) + .send() + .await + .with_context(|| format!("failed to fetch secret '{value}' from Secrets Manager"))? + .secret_string() + .with_context(|| format!("secret '{value}' has no string value")) + .map(|v| v.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_aws_sm_uri_extracts_id_and_key() { + let (id, key) = parse_aws_sm_uri("aws-sm://oab/telegram/pahudxbot#TELEGRAM_BOT_TOKEN") + .unwrap() + .unwrap(); + assert_eq!(id, "oab/telegram/pahudxbot"); + assert_eq!(key, "TELEGRAM_BOT_TOKEN"); + } + + #[test] + fn parse_aws_sm_uri_rejects_missing_hash() { + assert!(parse_aws_sm_uri("aws-sm://oab/telegram/pahudxbot").unwrap().is_err()); + } + + #[test] + fn parse_aws_sm_uri_rejects_empty_parts() { + assert!(parse_aws_sm_uri("aws-sm://#key").unwrap().is_err()); + assert!(parse_aws_sm_uri("aws-sm://secret-id#").unwrap().is_err()); + } + + #[test] + fn parse_aws_sm_uri_returns_none_for_other_schemes() { + assert!(parse_aws_sm_uri("arn:aws:secretsmanager:us-east-1:123:secret:oab/x-AbCdEf").is_none()); + assert!(parse_aws_sm_uri("plain-secret-name").is_none()); + } +} From eea64e46a76a5f94481b91399e2c677039802ff5 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 3 Jul 2026 23:05:43 -0400 Subject: [PATCH 2/9] docs(operator): clarify aws-sm:// works for any spec.secrets key Both formats (ECS-native ARN valueFrom, or the aws-sm://# shorthand) are accepted per-value, not tied to a specific env var name - TELEGRAM_BOT_TOKEN (or any other key) can use either format. The previous example picked a different key for each format, which read as if the format choice depended on which secret it was. --- operator/README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/operator/README.md b/operator/README.md index b139ae8b0..723248945 100644 --- a/operator/README.md +++ b/operator/README.md @@ -411,20 +411,25 @@ Attached to `oab-task-role` — the identity the *running container* assumes. `spec.secrets` maps a container env var name to where ECS should fetch its value from at task launch (via the execution role below — the container itself never sees a raw secret reference, only the resolved value). Two -formats are accepted, and can be mixed within the same manifest: +formats are accepted **per value** — either works for any key, and both can +be mixed freely across different keys in the same manifest: ```yaml spec: secrets: - # ECS-native valueFrom: a full Secrets Manager ARN. Add a `:::` - # suffix to extract one field of a JSON secret; omit it for a - # plain-string secret. + # Format 1 — ECS-native valueFrom: a full Secrets Manager ARN. Add a + # `:::` suffix to extract one field of a JSON secret; omit it + # for a plain-string secret. TELEGRAM_BOT_TOKEN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:oab/telegram/mybot-AC80TP:TELEGRAM_BOT_TOKEN::" - # aws-sm://# shorthand — the same convention + + # Format 2 — aws-sm://# shorthand, equivalent to + # the line above (same secret, same key). This is the same convention # openab itself uses for in-app secret refs in config.toml (see # docs/secrets-management.md). oabctl resolves this to the ECS-native - # form above automatically; can be a bare secret name - # (resolved to its ARN via DescribeSecret) or a full ARN directly. + # form automatically; can be a bare secret name (resolved + # to its ARN via DescribeSecret) or a full ARN directly. + # TELEGRAM_BOT_TOKEN: "aws-sm://oab/telegram/mybot#TELEGRAM_BOT_TOKEN" + TELEGRAM_SECRET_TOKEN: "aws-sm://oab/telegram/mybot#TELEGRAM_SECRET_TOKEN" ``` From 4bbdaf43a1156f680e4d02c0453a50f1730b3372 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 3 Jul 2026 23:08:52 -0400 Subject: [PATCH 3/9] docs(operator): spell out TELEGRAM_BOT_TOKEN aws-sm:// example explicitly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous revision buried this as a commented-out alternate line, which undersold the point — state directly, in active prose, that TELEGRAM_BOT_TOKEN (the example key actually used with the ARN format) supports aws-sm:// too, and vice versa for TELEGRAM_SECRET_TOKEN. --- operator/README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/operator/README.md b/operator/README.md index 723248945..fceb58d80 100644 --- a/operator/README.md +++ b/operator/README.md @@ -422,17 +422,19 @@ spec: # for a plain-string secret. TELEGRAM_BOT_TOKEN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:oab/telegram/mybot-AC80TP:TELEGRAM_BOT_TOKEN::" - # Format 2 — aws-sm://# shorthand, equivalent to - # the line above (same secret, same key). This is the same convention - # openab itself uses for in-app secret refs in config.toml (see - # docs/secrets-management.md). oabctl resolves this to the ECS-native - # form automatically; can be a bare secret name (resolved - # to its ARN via DescribeSecret) or a full ARN directly. - # TELEGRAM_BOT_TOKEN: "aws-sm://oab/telegram/mybot#TELEGRAM_BOT_TOKEN" - + # Format 2 — aws-sm://# shorthand: the same + # convention openab itself uses for in-app secret refs in config.toml + # (see docs/secrets-management.md). oabctl resolves this to the + # ECS-native form automatically; can be a bare secret name + # (resolved to its ARN via DescribeSecret) or a full ARN directly. TELEGRAM_SECRET_TOKEN: "aws-sm://oab/telegram/mybot#TELEGRAM_SECRET_TOKEN" ``` +Either format works for **any** key — `TELEGRAM_BOT_TOKEN` above could just +as well be written `"aws-sm://oab/telegram/mybot#TELEGRAM_BOT_TOKEN"`, and +`TELEGRAM_SECRET_TOKEN` could just as well use the full ARN form. The choice +is per-value, not tied to which secret it is. + ### IAM Execution Role Permissions Attached to `oab-task-execution` — the identity **ECS itself** assumes to pull From 10314dda78b213ffd13877220ff80164a1f36fa1 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 3 Jul 2026 23:09:52 -0400 Subject: [PATCH 4/9] docs(operator): link to official Telegram docs for secret_token hardening Clarifies TELEGRAM_SECRET_TOKEN maps directly to Telegram's own setWebhook secret_token parameter (not an openab-specific invention) and links to core.telegram.org/bots/api#setwebhook for the canonical description. --- operator/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/operator/README.md b/operator/README.md index fceb58d80..06687aad6 100644 --- a/operator/README.md +++ b/operator/README.md @@ -277,7 +277,10 @@ must still be registered manually in the LINE Developers console. > request's source IP against Telegram's published webhook subnets; LINE > verifies an HMAC-SHA256 signature using `LINE_CHANNEL_SECRET`. Set > `TELEGRAM_SECRET_TOKEN` in `spec.secrets` to enable that check — `apply` -> passes it to Telegram automatically as described above. +> passes it to Telegram automatically as described above. `TELEGRAM_SECRET_TOKEN` +> is Telegram's own `secret_token` hardening mechanism for `setWebhook`, not an +> openab-specific convention — see the [official Telegram Bot API +> docs](https://core.telegram.org/bots/api#setwebhook) for details. > > **Stage prefix stripped before it reaches the backend:** for private From 1b842135db34d0219b353838d0dbe56188eeb118 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 3 Jul 2026 23:11:54 -0400 Subject: [PATCH 5/9] docs(operator): add inline aws-sm:// alternative comment under TELEGRAM_BOT_TOKEN Immediately below the ARN example, so readers see at a glance that aws-sm:// is a drop-in alternative for that exact key, not just something explained separately further down. --- operator/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/operator/README.md b/operator/README.md index 06687aad6..c63a650c5 100644 --- a/operator/README.md +++ b/operator/README.md @@ -424,6 +424,7 @@ spec: # `:::` suffix to extract one field of a JSON secret; omit it # for a plain-string secret. TELEGRAM_BOT_TOKEN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:oab/telegram/mybot-AC80TP:TELEGRAM_BOT_TOKEN::" + # TELEGRAM_BOT_TOKEN: "aws-sm://oab/telegram/mybot#TELEGRAM_BOT_TOKEN" # equivalent, Format 2 below # Format 2 — aws-sm://# shorthand: the same # convention openab itself uses for in-app secret refs in config.toml From cdbbe636624abb70962a0a570a24ff34418d4f22 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 3 Jul 2026 23:14:33 -0400 Subject: [PATCH 6/9] docs(operator): explain what TELEGRAM_BOT_TOKEN/TELEGRAM_SECRET_TOKEN are for The spec.secrets format section only explained the two ARN/aws-sm:// value formats, never what the example keys themselves actually do. Add inline comments directly above each key stating its purpose (BotFather API token vs. optional setWebhook signing hardening) so a reader doesn't have to piece it together from other sections. --- operator/README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/operator/README.md b/operator/README.md index c63a650c5..1265fa736 100644 --- a/operator/README.md +++ b/operator/README.md @@ -415,17 +415,28 @@ Attached to `oab-task-role` — the identity the *running container* assumes. value from at task launch (via the execution role below — the container itself never sees a raw secret reference, only the resolved value). Two formats are accepted **per value** — either works for any key, and both can -be mixed freely across different keys in the same manifest: +be mixed freely across different keys in the same manifest. The example +below uses the two Telegram-specific keys from "Auto-register the Telegram +webhook" above, annotated with what each is for: ```yaml spec: secrets: + # TELEGRAM_BOT_TOKEN — the bot's API token from BotFather. Required for + # openab to authenticate as the bot, and for `apply` to auto-register + # the webhook via setWebhook (see above). + # # Format 1 — ECS-native valueFrom: a full Secrets Manager ARN. Add a # `:::` suffix to extract one field of a JSON secret; omit it # for a plain-string secret. TELEGRAM_BOT_TOKEN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:oab/telegram/mybot-AC80TP:TELEGRAM_BOT_TOKEN::" # TELEGRAM_BOT_TOKEN: "aws-sm://oab/telegram/mybot#TELEGRAM_BOT_TOKEN" # equivalent, Format 2 below + # TELEGRAM_SECRET_TOKEN — optional. Telegram's own setWebhook + # secret_token request-signing hardening (see the security note above), + # not an openab-specific convention. `apply` passes it to Telegram + # automatically if present. + # # Format 2 — aws-sm://# shorthand: the same # convention openab itself uses for in-app secret refs in config.toml # (see docs/secrets-management.md). oabctl resolves this to the From 8772dfdd30cbda6a2b96af2ddfa8a935639596c4 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 3 Jul 2026 23:17:38 -0400 Subject: [PATCH 7/9] docs(operator): add minimal working Telegram+ingress manifest example The existing ingress example is a partial spec: snippet illustrating the schema (both Telegram and LINE paths, non-default values). Add a separate, complete, copy-pasteable manifest showing only the fields actually required by the schema (apiVersion/kind/metadata, image, resources, configFrom, runtime.networking, ingress.paths) plus the one practically-required field for Telegram to work (TELEGRAM_BOT_TOKEN in spec.secrets, using the aws-sm:// shorthand), so a reader can go straight from README to a working oabctl apply without cross-referencing multiple examples. --- operator/README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/operator/README.md b/operator/README.md index 1265fa736..792124a43 100644 --- a/operator/README.md +++ b/operator/README.md @@ -242,6 +242,42 @@ spec: - /webhook/line ``` +#### Minimal working manifest: Telegram on AWS + +The complete set of fields actually required to deploy a Telegram bot with +ingress — everything else in the schema has a default. Save as a file and +`oabctl apply -f `: + +```yaml +apiVersion: oab.dev/v2 +kind: OABService +metadata: + name: my-telegram-bot + namespace: prod +spec: + image: ghcr.io/openabdev/openab:latest + resources: { cpu: "256", memory: "512" } + configFrom: s3:///config.toml + secrets: + TELEGRAM_BOT_TOKEN: "aws-sm://oab/telegram/my-telegram-bot#TELEGRAM_BOT_TOKEN" + runtime: + type: ecs + networking: + subnets: [subnet-aaa, subnet-bbb] + securityGroups: [sg-xxx] + ingress: + paths: + - /webhook/telegram +``` + +`spec.secrets.TELEGRAM_BOT_TOKEN` isn't required by the schema (`spec.secrets` +defaults to empty), but without it the bot has no way to authenticate to +Telegram and `apply` has nothing to call `setWebhook` with — include it for +Telegram to actually work. `configFrom` must point at a `config.toml` with a +matching `[telegram]` block (`webhook_path = "/webhook/telegram"`, +`bot_token = "${TELEGRAM_BOT_TOKEN}"`) — see +[docs/telegram.md](../docs/telegram.md) for the full config reference. + On `apply` this reconciles (idempotently, reused by name): 1. **Cloud Map** private DNS namespace (`-`, shared per-VPC) + a per-service **SRV** record (carries the container port; a plain A record does not work as a VPC-Link integration target) From 868569ec471c5fc9fb462adfa6c58ef718e4308c Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 3 Jul 2026 23:19:38 -0400 Subject: [PATCH 8/9] docs(operator): use the official ghcr.io/openabdev/openab image everywhere public.ecr.aws/oablab/kiro was a stale pre-unification image reference - every other doc in the repo (image-tags.md, aws-ecs-fargate-spot.md, agentcore.md, etc.) already uses ghcr.io/openabdev/openab:. Fixes the 3 remaining occurrences in operator/README.md for consistency with the new minimal manifest example, which already used the correct registry. --- operator/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/operator/README.md b/operator/README.md index 792124a43..d36ac2e44 100644 --- a/operator/README.md +++ b/operator/README.md @@ -196,7 +196,7 @@ metadata: name: my-bot namespace: prod spec: - image: public.ecr.aws/oablab/kiro:stable + image: ghcr.io/openabdev/openab:stable resources: cpu: "256" memory: "512" @@ -224,7 +224,7 @@ linked here. ```yaml spec: - image: public.ecr.aws/oablab/kiro:beta + image: ghcr.io/openabdev/openab:beta resources: { cpu: "256", memory: "512" } configFrom: s3://.../config.toml runtime: @@ -386,7 +386,7 @@ metadata: namespace: prod spec: template: - image: public.ecr.aws/oablab/kiro:stable + image: ghcr.io/openabdev/openab:stable resources: { cpu: "256", memory: "512" } runtime: type: ecs From f2325905f05ca8e40fd59a3c35e4b347633491c8 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 3 Jul 2026 23:20:48 -0400 Subject: [PATCH 9/9] docs(operator): use agent-suffixed image tags (stable-kiro/beta-kiro) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per docs/image-tags.md: there is no default agent and no bare stable/beta/latest tag — every tag must specify an agent (-). Fixes all 4 image references in this file to use stable-kiro or beta-kiro instead of the now-invalid bare tags. --- operator/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/operator/README.md b/operator/README.md index d36ac2e44..c98d282dc 100644 --- a/operator/README.md +++ b/operator/README.md @@ -196,7 +196,7 @@ metadata: name: my-bot namespace: prod spec: - image: ghcr.io/openabdev/openab:stable + image: ghcr.io/openabdev/openab:stable-kiro resources: cpu: "256" memory: "512" @@ -224,7 +224,7 @@ linked here. ```yaml spec: - image: ghcr.io/openabdev/openab:beta + image: ghcr.io/openabdev/openab:beta-kiro resources: { cpu: "256", memory: "512" } configFrom: s3://.../config.toml runtime: @@ -255,7 +255,7 @@ metadata: name: my-telegram-bot namespace: prod spec: - image: ghcr.io/openabdev/openab:latest + image: ghcr.io/openabdev/openab:stable-kiro resources: { cpu: "256", memory: "512" } configFrom: s3:///config.toml secrets: @@ -386,7 +386,7 @@ metadata: namespace: prod spec: template: - image: ghcr.io/openabdev/openab:stable + image: ghcr.io/openabdev/openab:stable-kiro resources: { cpu: "256", memory: "512" } runtime: type: ecs