Skip to content

Commit 9d19013

Browse files
authored
feat: add workflow-scoped principals (#1107)
* feat: add workflow-scoped principals * feat: add workflow host sandbox helm value * chore: bump centaur chart * fix: default workflow host sandbox chart value off * fix: fail closed for workflow principals * fix: label workflow principals by kind * fix: simplify workflow principal labels
1 parent ad88a20 commit 9d19013

14 files changed

Lines changed: 398 additions & 12 deletions

File tree

contrib/chart/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: centaur
33
description: Helm chart for the trusted Centaur control plane
44
type: application
5-
version: 0.1.104
5+
version: 0.1.105
66
appVersion: "0.1.0"
77
dependencies:
88
- name: connect

contrib/chart/templates/apirs.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ spec:
249249
- name: KUBERNETES_WORKFLOW_DIRS
250250
value: {{ $sandboxWorkflowDirs | quote }}
251251
{{- end }}
252+
{{- if not (hasKey .Values.apiRs.extraEnv "WORKFLOW_HOST_SANDBOX") }}
253+
- name: WORKFLOW_HOST_SANDBOX
254+
value: {{ .Values.apiRs.workflowHostSandbox | quote }}
255+
{{- end }}
252256
{{- if not (hasKey .Values.apiRs.extraEnv "WORKFLOW_ENABLE_MODE") }}
253257
- name: WORKFLOW_ENABLE_MODE
254258
value: {{ .Values.apiRs.workflowEnableMode | quote }}

contrib/chart/values.schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@
267267
"mcpPublicUrl": { "type": "string" },
268268
"sandboxRunningLimit": { "type": "integer", "minimum": 0 },
269269
"sandboxHotIdleGraceSecs": { "type": "integer", "minimum": 0 },
270+
"workflowHostSandbox": { "type": "boolean" },
270271
"etl": {
271272
"type": "object",
272273
"properties": {

contrib/chart/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ apiRs:
372372
# Workflow enablement policy. Production defaults to all workflows enabled.
373373
# Set workflowEnableMode: allowlist in staging and list allowed workflow names
374374
# in workflowAllowedNames as a comma/whitespace-separated string.
375+
workflowHostSandbox: true
375376
workflowEnableMode: all
376377
workflowAllowedNames: ""
377378
# Scheduled ETL workflow configuration. The chart renders these into api-rs

docs/pages/extend/workflows-v2.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Supported v2 primitives:
6464
| `ctx._pool` | Supported when the workflow-host sandbox receives `DATABASE_URL` |
6565
| `WEBHOOKS` | Supported |
6666
| `SCHEDULE` | Supported |
67+
| `WORKFLOW_PRINCIPAL` | Supported for workflow-host sandbox tool permissions |
6768

6869
## Required migrations
6970

@@ -120,6 +121,32 @@ The workflow host sandbox is separate from the agent sandbox. The workflow
120121
handler coordinates the run; the agent turn runs through the normal Centaur
121122
session runtime.
122123

124+
#### Declare Workflow-Host Permissions
125+
126+
When a workflow calls tools directly from the workflow host with
127+
`ctx.call_tool(...)`, declare the principal that should own those permissions:
128+
129+
```python
130+
WORKFLOW_NAME = "nightly_report"
131+
WORKFLOW_PRINCIPAL = True
132+
```
133+
134+
The API derives and registers the `workflow-nightly-report` principal in the
135+
Centaur Console and runs that workflow's host sandbox under it. Grant only the
136+
roles or secrets that workflow needs:
137+
138+
```bash
139+
cargo run -p centaur-perms -- \
140+
principals grant workflow-nightly-report \
141+
--tool slack
142+
```
143+
144+
The principal id is always `workflow-` plus the slugged `WORKFLOW_NAME`.
145+
Workflow code cannot choose another principal id, display name, or labels.
146+
`WORKFLOW_PRINCIPAL = True` requires `apiRs.workflowHostSandbox=true`, which
147+
renders `WORKFLOW_HOST_SANDBOX=true`; startup fails if a workflow declares a
148+
principal while workflow-host sandboxing is disabled.
149+
123150
#### Pick the model and reasoning effort
124151

125152
`ctx.agent_turn(...)` accepts optional `model`, `provider`, and `reasoning`

docs/pages/extend/workflows.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ from api.workflow_engine import WorkflowContext
4545

4646
WORKFLOW_NAME = "nightly_report"
4747

48+
WORKFLOW_PRINCIPAL = True
49+
4850

4951
@dataclass
5052
class Input:
@@ -62,6 +64,16 @@ async def handler(inp: Input, ctx: WorkflowContext) -> dict[str, Any]:
6264
return {"channel": inp.channel, "report": result}
6365
```
6466

67+
`WORKFLOW_PRINCIPAL` is optional. Use it when the workflow host calls tools
68+
directly with `ctx.call_tool(...)` and should have its own credential boundary.
69+
The API derives and registers the `workflow-nightly-report` principal from
70+
`WORKFLOW_NAME` and runs that workflow-host sandbox under it. Workflow code
71+
cannot choose another principal id, display name, or labels. Grant the required
72+
tool roles or secrets to the derived principal. `WORKFLOW_PRINCIPAL = True`
73+
requires `apiRs.workflowHostSandbox=true`, which renders
74+
`WORKFLOW_HOST_SANDBOX=true`; startup fails if workflow-host sandboxing is
75+
disabled.
76+
6577
## Durable primitives
6678

6779
| Primitive | Use it for |

docs/pages/reference/configuration.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Optional required-by-mode variables:
8181
| `CENTAUR_ENVIRONMENT`, `DEPLOY_ENV`, `ENVIRONMENT` | `apiRs.extraEnv` or deployment env. | Deployment environment resource attribute for telemetry. |
8282
| `OTEL_TRACES_EXPORTER` | `apiRs.extraEnv`. | Set to `otlp` to force OTLP trace export, or `none`/`off` to disable it. |
8383
| `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `apiRs.extraEnv`. | Enables OTLP trace export to Tempo, Jaeger, or another OTLP collector. |
84+
| `apiRs.workflowHostSandbox`, `WORKFLOW_HOST_SANDBOX` | Helm value, default `true`; override with `apiRs.extraEnv`. | Runs workflow hosts in Kubernetes sandboxes instead of the api-rs process. Required for workflow-scoped principals. |
8485
| `apiRs.metrics.scrapeAnnotations` | Helm value, default `true`. | Adds Prometheus scrape annotations to the API-RS Pod template and Service. |
8586
| `apiRs.metrics.path` | Helm value, default `/metrics`. | Metrics scrape path for annotation-based discovery. |
8687
| `apiRs.metrics.annotations` | Helm value. | Additional scrape annotations for Prometheus-compatible collectors. |

services/api-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/api-rs/crates/centaur-api-server/src/args.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use centaur_session_core::HarnessType;
3333
use centaur_session_runtime::{
3434
PersonaRegistry, SandboxCapacityConfig, SandboxWorkloadMode, SessionSandboxCleanupConfig,
3535
};
36-
use centaur_workflows::WorkflowHostSandboxRuntime;
36+
use centaur_workflows::{WorkflowHostSandboxRuntime, WorkflowPrincipalRegistrar};
3737
use clap::{Args as ClapArgs, Parser, ValueEnum};
3838
use tracing::{info, warn};
3939

@@ -123,6 +123,7 @@ pub(crate) struct IronControlRuntime {
123123
pub(crate) registrar: SessionRegistrar,
124124
pub(crate) warm_pool_bootstrap_principal: String,
125125
pub(crate) workflow_host_principal: String,
126+
pub(crate) workflow_principal_registrar: WorkflowPrincipalRegistrar,
126127
}
127128

128129
#[derive(Debug, ClapArgs)]
@@ -723,9 +724,10 @@ impl SandboxArgs {
723724
client.assign_role(&workflow_host.id, role_id).await?;
724725
}
725726
Ok(Some(IronControlRuntime {
726-
registrar: SessionRegistrar::new(client, namespace, role_ids),
727+
registrar: SessionRegistrar::new(client.clone(), namespace.clone(), role_ids),
727728
warm_pool_bootstrap_principal: bootstrap.id,
728729
workflow_host_principal: workflow_host.id,
730+
workflow_principal_registrar: WorkflowPrincipalRegistrar::new(client, namespace),
729731
}))
730732
}
731733

services/api-rs/crates/centaur-api-server/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,12 @@ async fn initialize_runtime(args: Args, app_state: AppState) -> Result<(), Serve
7979
.with_openai_session_title_generator_from_env();
8080
let mut warm_pool_bootstrap_principal = None;
8181
let mut workflow_host_principal = None;
82+
let mut workflow_principal_registrar = None;
8283
if let Some(iron_control) = args.iron_control_runtime().await? {
8384
info!("iron-control session registration enabled");
8485
warm_pool_bootstrap_principal = Some(iron_control.warm_pool_bootstrap_principal);
8586
workflow_host_principal = Some(iron_control.workflow_host_principal);
87+
workflow_principal_registrar = Some(iron_control.workflow_principal_registrar);
8688
runtime = runtime.with_iron_control(iron_control.registrar);
8789
}
8890
runtime = runtime.with_personas(args.persona_registry()?);
@@ -100,10 +102,11 @@ async fn initialize_runtime(args: Args, app_state: AppState) -> Result<(), Serve
100102
.workflow_host_sandbox_runtime(workflow_host_principal.as_deref())
101103
.await?;
102104
let workflows = Some(
103-
WorkflowRuntime::new_with_workflow_host_sandbox(
105+
WorkflowRuntime::new_with_workflow_host_sandbox_and_principal_registrar(
104106
store,
105107
runtime.clone(),
106108
workflow_host_sandbox,
109+
workflow_principal_registrar,
107110
)
108111
.await?,
109112
);

0 commit comments

Comments
 (0)