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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ spark setup telegram-voice-starter

Add `--elevenlabs-api-key @clipboard` if you want hosted ElevenLabs TTS configured during setup. The key is stored in Spark secrets and injected into Builder at runtime; it is not written into Telegram config.

For hosted OpenAI transcription or Realtime voice, store the dedicated Voice key first, then resume the voice bundle setup:

```bash
spark secrets set voice.openai.api_key
spark setup telegram-voice-starter --resume
```

Spark injects that key as `VOICE_OPENAI_API_KEY` only across the installed Voice boundary. It does not reuse the Agent role's `OPENAI_API_KEY`, which may belong to another OpenAI-compatible provider.

To rotate the local Telegram↔Spawner control key without putting it in command history:

```bash
spark secrets set spark.bridge_api_key --generate
```

Spark stages the replacement in managed secret storage, stops only the currently running bridge consumers, promotes one shared generation, starts Spawner first, and restores exactly the Telegram profiles that were running. A failed health check rolls the generation and consumer set back.

Public builder labs such as `spark-domain-chip-labs` and `spark-personality-chip-labs` are available separately, but they are not automatic starter-bundle modules yet. Spark Swarm Workspace/network submission is private/upcoming and is not required for local recursive Builder chip loops.

The current installer proof lane covers 11 canonical repos: `spark-cli` plus the 10 registry-pinned runtime/support modules. The plain `telegram-starter` bundle exercises Harness Core, Researcher, Character, Builder, Memory, Spawner, and Telegram directly. `telegram-voice-starter`, QA Evidence Lane, and Skill Graphs require their own optional-module proof before claiming the entire 11-repo lane is ship-ready.
Expand Down
7 changes: 7 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ generated config, terminal output, and model-visible context.
tests.
- Generated module env files must not contain raw cloud API keys when the module
declares the matching keychain-backed secret.
- Hosted OpenAI voice uses the dedicated `voice.openai.api_key` secret. Spark
exposes it as `VOICE_OPENAI_API_KEY` only to the configured Voice path instead
of reusing the Agent role's potentially different `OPENAI_API_KEY`.
- The local Telegram↔Spawner key is stored as `spark.bridge_api_key` and injected
only into those two runtime consumers. Rotate it with
`spark secrets set spark.bridge_api_key --generate`; the value is never placed
in argv, generated module env files, process records, or command output.

## Files That Must Never Be Committed

Expand Down
64 changes: 64 additions & 0 deletions src/spark_cli/bridge_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"SPARK_UI_API_KEY",
"TELEGRAM_RELAY_SECRET",
}
BRIDGE_API_KEY_ENV = "SPARK_BRIDGE_API_KEY"
BRIDGE_API_KEY_SECRET_ID = "spark.bridge_api_key"
BRIDGE_API_KEY_PENDING_SECRET_ID = "spark.bridge_api_key.pending"
BRIDGE_CONSUMER_MODULES = frozenset({"spawner-ui", "spark-telegram-bot"})


def load_generated_bridge_envs(
Expand Down Expand Up @@ -73,3 +77,63 @@ def resolve_shared_spawner_bridge_api_key(
"SPARK_BRIDGE_API_KEY must be different from UI, relay, provider, and other control secrets."
)
return bridge_key


def resolve_existing_bridge_api_key(
generated_envs: Mapping[str, Mapping[str, str]],
*,
stored: str = "",
parent: str = "",
forbidden_secrets: Iterable[str] = (),
parent_control_values: Iterable[str] = (),
token_factory: Callable[[], str] | None = None,
) -> str:
"""Resolve local migration precedence without letting ambient env hide drift."""
stored = stored.strip()
parent = parent.strip()
if stored:
return resolve_shared_spawner_bridge_api_key(
generated_envs,
explicit=stored,
forbidden_secrets=forbidden_secrets,
parent_control_values=parent_control_values,
token_factory=token_factory,
)
generated_values = {
str(values.get(BRIDGE_API_KEY_ENV) or "").strip()
for values in generated_envs.values()
if str(values.get(BRIDGE_API_KEY_ENV) or "").strip()
}
if generated_values:
return resolve_shared_spawner_bridge_api_key(
generated_envs,
forbidden_secrets=forbidden_secrets,
parent_control_values=parent_control_values,
token_factory=token_factory,
)
return resolve_shared_spawner_bridge_api_key(
generated_envs,
explicit=parent,
forbidden_secrets=forbidden_secrets,
parent_control_values=parent_control_values,
token_factory=token_factory,
)


def bridge_consumer_process_keys(pids: Mapping[str, object]) -> list[str]:
"""Return bridge consumers in safe stop order: Telegram profiles, then Spawner."""
telegram = sorted(
key
for key in pids
if key == "spark-telegram-bot" or key.startswith("spark-telegram-bot:")
)
spawner = ["spawner-ui"] if "spawner-ui" in pids else []
return [*telegram, *spawner]


def bridge_consumer_start_order(process_keys: Iterable[str]) -> list[str]:
"""Return safe start order: Spawner, then the exact prior Telegram profiles."""
keys = set(process_keys)
result = ["spawner-ui"] if "spawner-ui" in keys else []
result.extend(sorted(key for key in keys if key.startswith("spark-telegram-bot")))
return result
Loading
Loading