Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
379b95d
feat: batch company context projections (#1055)
akshaan Jul 13, 2026
32061ce
fix(slackbotv2): support scoped trigger bot IDs (#1051)
svc-paradigm Jul 13, 2026
8357943
fix: include API controllers in console request logs (#1056)
mslipper Jul 13, 2026
ab8c8b2
fix: remove company context size metric (#1057)
akshaan Jul 13, 2026
bf8bdc8
fix(console): keep composer model picker menu inside the viewport (#1…
decofe Jul 13, 2026
a6a7c0a
fix(console): preserve requester attribution for PRs (#1054)
decofe Jul 13, 2026
d0271e2
feat: add Airtable record write operations (#1058)
mslipper Jul 13, 2026
ee55a37
fix: allow hosted MCP OAuth redirects (#1059)
mslipper Jul 13, 2026
91a619b
fix: fall back to direct Slack threads (#1061)
mslipper Jul 13, 2026
4e6c13b
feat: sync Granola via user OAuth (#1060)
akshaan Jul 13, 2026
1a2de01
feat: add Airtable identity health check (#1062)
mslipper Jul 13, 2026
c86576c
feat: expose sandbox permission query (#1039)
mslipper Jul 14, 2026
f8fefed
feat: add centaur console permissions tool (#1065)
mslipper Jul 14, 2026
2f631f0
feat: project Granola notes into context (#1063)
akshaan Jul 14, 2026
8c4e21b
fix(slackbot): trigger on rich message mentions (#1048)
decofe Jul 14, 2026
7e5c8ef
feat: harden console public login (#1072)
mslipper Jul 14, 2026
4642964
fix: require console login for oauth credential minting (#1074)
mslipper Jul 14, 2026
bc9a016
fix: harden console public access controls (#1076)
mslipper Jul 14, 2026
f05decd
feat: ingest private Slack channels from OAuth (#1079)
akshaan Jul 14, 2026
20a9222
feat: add console chat privacy controls (#1078)
goksu Jul 14, 2026
3e1bdd1
feat(console): make the operator console responsive on mobile (#1084)
cjustice Jul 15, 2026
5d82a00
fix(console): scope while-running transcript refresh to the active pa…
decofe Jul 15, 2026
ed947f9
feat: expose sandbox oauth connection discovery (#1086)
mslipper Jul 15, 2026
e96e465
feat(slackbotv2): show Codex effort and speed (#1085)
goksu Jul 15, 2026
6458e15
feat(slackbotv2): default model and reasoning effort per Slack channe…
cjustice Jul 15, 2026
12fd0bf
feat(workflows): plumb model and reasoning effort through ctx.agent_t…
cjustice Jul 15, 2026
63e12f5
docs: clarify cron weekday numbering (#1089)
goksu Jul 15, 2026
373e92c
fix: support non-rotating Slack OAuth tokens (#1088)
mslipper Jul 15, 2026
c4fd532
Merge Paradigm Centaur upstream through #1087
Jul 15, 2026
1984802
Merge latest Paradigm Centaur upstream
Jul 15, 2026
43d0d73
Document private Slack migration cutover
fineas-bot[bot] Jul 15, 2026
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
5 changes: 3 additions & 2 deletions .agents/skills/company-context/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: company-context
description: "Use Centaur's indexed company context together with direct Slack, Linear, Google Docs, Drive, or Calendar searches when answering internal company-history, prior-decision, project-context, meeting-context, roadmap/status, or cross-source memory questions. Indexed context includes Slack channels, user-visible Slack DMs, Google Docs, Google Calendar, and Linear. Use for questions like what was discussed, decided, planned, mentioned, or documented internally, especially when the user did not name one exact source."
description: "Use Centaur's indexed company context together with direct Slack, Linear, Google Docs, Drive, Calendar, or Granola searches when answering internal company-history, prior-decision, project-context, meeting-context, roadmap/status, or cross-source memory questions. Indexed context includes Slack channels, user-visible Slack DMs, Google Docs, Google Calendar, Linear, and user-visible Granola notes. Use for questions like what was discussed, decided, planned, mentioned, or documented internally, especially when the user did not name one exact source."
---

# Company Context

Use `company_context` as the first retrieval step for internal historical context. Its `search` command queries indexed company memory across enabled sources such as Slack channels, Google Docs (`--source docs`), Google Calendar, and Linear. It also has dedicated commands for user-visible Slack DMs and DM conversations. Always pair indexed results with the relevant direct source tools, then reconcile and collate both evidence sets before answering.
Use `company_context` as the first retrieval step for internal historical context. Its `search` command queries indexed company memory across enabled sources such as Slack channels, Google Docs (`--source docs`), Google Calendar, Linear, and user-visible Granola notes (`--source granola`). It also has dedicated commands for user-visible Slack DMs and DM conversations. Always pair indexed results with the relevant direct source tools, then reconcile and collate both evidence sets before answering.

## Default Workflow

Expand Down Expand Up @@ -57,6 +57,7 @@ company_context search "QUERY" --source slack --limit 10 --json
company_context search "QUERY" --source docs --source-type google_doc --limit 10 --json
company_context search "QUERY" --source google_calendar --limit 10 --json
company_context search "QUERY" --source linear --limit 10 --json
company_context search "QUERY" --source granola --limit 10 --json
company_context search-dms "QUERY" --limit 10 --json
```

Expand Down
2 changes: 1 addition & 1 deletion contrib/chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: centaur
description: Helm chart for the trusted Centaur control plane
type: application
version: 0.1.98
version: 0.1.103
appVersion: "0.1.0"
dependencies:
- name: connect
Expand Down
1 change: 1 addition & 0 deletions contrib/chart/templates/apirs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
(dict "name" "COMPANY_CONTEXT_DOCUMENTS_ENABLED" "value" (dig "companyContextDocuments" "enabled" true $apiRsEtl))
(dict "name" "COMPANY_CONTEXT_DOCUMENTS_INTERVAL_SECONDS" "value" (dig "companyContextDocuments" "intervalSeconds" 14400 $apiRsEtl))
(dict "name" "COMPANY_CONTEXT_DOCUMENTS_MAX_WINDOW_SECONDS" "value" (dig "companyContextDocuments" "maxWindowSeconds" 21600 $apiRsEtl))
(dict "name" "COMPANY_CONTEXT_DOCUMENTS_BATCH_SIZE" "value" (dig "companyContextDocuments" "batchSize" 50 $apiRsEtl))
-}}
{{- $apiRsEtlPassthroughNames := list -}}
{{- range $env := $apiRsEtlEnv -}}
Expand Down
2 changes: 2 additions & 0 deletions contrib/chart/templates/console-worker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ spec:
{{- if .Values.apiRs.enabled }}
- name: CENTAUR_API_URL
value: {{ printf "http://%s:%v" $apiRsName .Values.apiRs.port | quote }}
# Console OAuth sync workers submit normalized provider data to the
# in-cluster API; credentials themselves never leave this pod.
- name: CENTAUR_CONSOLE_CENTAUR_API_URL
value: {{ printf "http://%s:%v" $apiRsName .Values.apiRs.port | quote }}
- name: CENTAUR_CONSOLE_CENTAUR_API_KEY
Expand Down
14 changes: 14 additions & 0 deletions contrib/chart/templates/console.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,27 @@ spec:
secretKeyRef:
name: {{ $secretEnv }}
key: {{ printf "%sCENTAUR_JWT_SIGNING_SECRET" $prefix }}
# In-cluster console URL exactly as sandboxes dial it (the same
# value api-rs receives as IRON_CONTROL_URL). Console derives the
# sandbox entitlements injection-rule host from it, so the rule
# matches regardless of what Host the sync request carried.
- name: CENTAUR_CONSOLE_URL
value: {{ include "centaur.consoleUrl" . | quote }}
{{- if $console.publicUrl }}
- name: CENTAUR_CONSOLE_PUBLIC_URL
value: {{ $console.publicUrl | quote }}
{{- end }}
{{- if $mcpPublicUrl }}
- name: CENTAUR_MCP_PUBLIC_URL
value: {{ $mcpPublicUrl | quote }}
{{- end }}
- name: CENTAUR_CONSOLE_PASSWORD_LOGIN_ENABLED
value: {{ $console.passwordLoginEnabled | quote }}
- name: CENTAUR_CONSOLE_PUBLIC_SLACK_THREADS_ENABLED
value: {{ $console.publicSlackThreadsEnabled | quote }}
{{- with $console.ssoEmailDomains }}
- name: CENTAUR_CONSOLE_SSO_EMAIL_DOMAINS
value: {{ join "," . | quote }}
{{- end }}
{{- if $console.googleOauth.enabled }}
# Google OAuth app credentials (sign-in + brokered token refresh).
Expand Down
19 changes: 14 additions & 5 deletions contrib/chart/templates/slackbotv2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,21 @@ spec:
- name: SLACKBOT_TRIGGER_BOT_ALLOWLIST
value: {{ .Values.slackbotv2.triggerBotAllowlist | quote }}
{{- end }}
{{- range $name := tuple "CLAUDE_MODEL" "CODEX_MODEL" }}
{{- if .Values.slackbotv2.channelDefaults }}
# Per-channel default harness / model / provider / reasoning, keyed
# by Slack conversation id (each value an object of harness/model/
# provider/reasoning fields, e.g. { harness: claude, model: opus }).
# Applied below an explicit/sticky per-thread flag and above the
# deployment/baked default, and forwarded onto the harness input line
# so it takes effect.
- name: SLACKBOTV2_CHANNEL_DEFAULTS
value: {{ .Values.slackbotv2.channelDefaults | toJson | quote }}
{{- end }}
{{- range $name := tuple "CLAUDE_MODEL" "CODEX_MODEL" "CODEX_MODEL_REASONING_EFFORT" }}
{{- if and (hasKey $.Values.sandbox.extraEnv $name) (not (hasKey $.Values.slackbotv2.extraEnv $name)) }}
# Mirror the deployer's harness default-model override
# (sandbox.extraEnv) so the Slack Console-link line names the model
# sandboxes actually run. slackbotv2.extraEnv wins if it sets the
# same variable.
# Mirror deployer harness display settings (sandbox.extraEnv) so
# the Slack Console-link line names the model/effort sandboxes
# actually run. slackbotv2.extraEnv wins when explicitly set.
- name: {{ $name }}
value: {{ index $.Values.sandbox.extraEnv $name | toString | quote }}
{{- end }}
Expand Down
3 changes: 2 additions & 1 deletion contrib/chart/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,8 @@
"properties": {
"enabled": { "type": "boolean" },
"intervalSeconds": { "type": "integer" },
"maxWindowSeconds": { "type": "integer" }
"maxWindowSeconds": { "type": "integer" },
"batchSize": { "type": "integer", "minimum": 1 }
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions contrib/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ console:
# When set, the slackbotv2 deployment also links the first assistant message
# in a Slack thread to the Console session view; leave empty to omit the link.
publicUrl: ""
# Break-glass email/password login. Disable when console is reachable from
# the public internet and SSO is configured.
passwordLoginEnabled: true
# When enabled, every authenticated Console user can browse conversations
# originating in public Slack channels. Private channels and DMs remain
# owner-only. Requires apiRs.etl.slack.enabled so channel privacy is synced;
# visibility fails closed without that catalog. Individual chats can still
# be shared by their owner from the Threads UI.
publicSlackThreadsEnabled: false
# Optional SSO admission policy. When non-empty, only identities whose email
# domain matches this list can sign in through Google or Slack.
ssoEmailDomains: []
image:
repository: centaur-console
tag: latest
Expand Down Expand Up @@ -448,6 +460,7 @@ apiRs:
enabled: true
intervalSeconds: 14400
maxWindowSeconds: 21600
batchSize: 50
# Reaper: stop sandboxes older than the max lifetime, regardless of whether
# they are running or suspended. 0 disables the sweep. Interval must be >= 1.
sandboxMaxLifetimeSecs: 259200 # 3 days
Expand Down Expand Up @@ -511,6 +524,20 @@ slackbotv2:
mcpPublicUrl: "http://localhost:3000"
externalOrgAllowlist: ""
triggerBotAllowlist: ""
# Per-channel default harness / model / provider / reasoning, keyed by Slack
# conversation id (the C…/G…/D… id). Each value is an object of optional
# harness / model / provider / reasoning fields, using the same vocabulary as
# the inline Slack flags (harness: claude|codex|amp, provider: bedrock|meta,
# model accepts Claude aliases like opus). A model is only meaningful within a
# harness, so name the harness alongside it. Applied when a message in that
# channel carries no explicit/sticky per-thread flag, below such a flag and
# above the deployment/baked harness default, and forwarded onto the harness
# input line so it takes effect. reasoning only affects the codex harness.
# Example:
# channelDefaults:
# C0ENG: { harness: claude, model: opus, reasoning: high }
# C0TRIAGE: { reasoning: low }
channelDefaults: {}
metrics:
# slackbotv2 serves Prometheus text metrics at /metrics. This flag only
# controls scrape annotations for Prometheus/VictoriaMetrics-style discovery.
Expand Down
43 changes: 42 additions & 1 deletion docs/pages/extend/workflows-v2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ result = await ctx.agent_turn(
"Investigate this alert and return the next action.",
thread_key=f"workflow:{ctx.run_id}:agent",
harness="codex",
model="gpt-5.2",
reasoning="high",
metadata={"workflow": WORKFLOW_NAME},
)
```
Expand All @@ -118,6 +120,41 @@ The workflow host sandbox is separate from the agent sandbox. The workflow
handler coordinates the run; the agent turn runs through the normal Centaur
session runtime.

Production deployments should keep `WORKFLOW_HOST_SANDBOX=true` (the default).
`false` runs Python as a child process of api-rs for local development and is
not an isolation boundary. Centaur removes its known control, bot, feedback,
GitHub, Slack, and model-provider credentials from that child, but arbitrary
ambient process configuration can still be visible; use only trusted workflow
code in local mode.

#### Pick the model and reasoning effort

`ctx.agent_turn(...)` accepts optional `model`, `provider`, and `reasoning`
kwargs. They ride the turn exactly like the Slack `--model` / `--bedrock` /
`-rsn` flags: `model` selects the model within the harness, `reasoning` sets the
codex reasoning effort (`none`/`minimal`/`low`/`medium`/`high`/`xhigh`/`max`),
and `provider` selects the codex model provider. `provider` and `reasoning` only
affect the codex harness; claude and amp ignore them. `reasoning` also accepts
the `reasoning_effort` and `effort` aliases. When a kwarg is omitted the
deployment/baked harness default stands — dispatched turns are no longer pinned
to the deployment default.

To set a default for **every** turn in a workflow, declare a module-level
`AGENT_DEFAULTS` dict. Explicit per-call kwargs override it key by key:

```python
WORKFLOW_NAME = "nightly_report"
AGENT_DEFAULTS = {"harness": "codex", "model": "gpt-5.2", "reasoning": "high"}

async def handler(inp: Input, ctx: WorkflowContext) -> dict[str, Any]:
await ctx.agent_turn("Draft the report.") # gpt-5.2 @ high
await ctx.agent_turn("Tidy formatting.", reasoning="low") # gpt-5.2 @ low
```

Keep `harness` and `model` together — a model is only meaningful within its
harness, and because kwargs override `AGENT_DEFAULTS` key by key, overriding one
without the other can strand a model on the wrong harness.

### Declare webhook metadata in the workflow

Expose a workflow through `WEBHOOKS`:
Expand Down Expand Up @@ -151,12 +188,16 @@ Schedules can live beside the handler:
```python
SCHEDULE = {
"type": "cron",
"cron": "0 9 * * 1-5",
"cron": "0 9 * * MON-FRI",
"timezone": "America/New_York",
"input": {"profile": "default"},
}
```

Write day-of-week as names (`MON-FRI`), not numbers: the parser is Quartz-style
(1 = Sunday), so a Unix-style `1-5` fires Sunday–Thursday. See
[Schedule a workflow](/extend/workflows#schedule-a-workflow) for details.

`api-rs` reconciles enabled schedule metadata into Absurd schedule tasks. ETL
workflows can be routed to a separate queue so long-running sync jobs do not
block normal workflow runs.
Expand Down
14 changes: 12 additions & 2 deletions docs/pages/extend/workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ async def handler(inp: Input, ctx: WorkflowContext) -> dict[str, Any]:
| `ctx.sleep(name, duration)` | Suspend and resume later. |
| `ctx.sleep_until(name, when)` | Resume at a specific time. |
| `ctx.wait_for_event(name, event_type, correlation_id)` | Wait for an external event. |
| `ctx.start_workflow(workflow_name, input, idempotency_key=...)` | Queue a child workflow and continue immediately; returns its durable task/run identifiers. |
| `ctx.wait_for_workflow(...)` | Wait for a child workflow to finish. |
| `ctx.run_workflow(...)` | Start and wait in one call. |
| `ctx.start_agent(...)` | Start an agent turn. |
Expand Down Expand Up @@ -129,7 +130,7 @@ WORKFLOW_NAME = "daily_market_digest"

SCHEDULE = {
"type": "cron",
"cron": "0 9 * * 1-5",
"cron": "0 9 * * MON-FRI",
"timezone": "America/New_York",
"input": {
"channel": "markets",
Expand All @@ -144,11 +145,20 @@ Cron schedules use five fields:
minute hour day-of-month month day-of-week
```

:::warning[Day-of-week numbering is Quartz-style, not Unix crontab]
The schedule engine parses cron expressions with the Rust
[`cron` crate](https://github.com/zslayton/cron), which numbers days of week
1–7 with **1 = Sunday** (`0` is rejected). A Unix-style `1-5` therefore fires
Sunday–Thursday, not Monday–Friday. Always write day-of-week as names
(`MON`, `MON-FRI`, `SAT,SUN`) — they mean the same thing in every dialect.
:::

Examples:

| Cron | Meaning |
|------|---------|
| `0 9 * * 1-5` | 9:00 AM every weekday. |
| `0 9 * * MON-FRI` | 9:00 AM every weekday. |
| `0 9 * * 1-5` | 9:00 AM Sunday–Thursday (Quartz numbering — probably not what you meant). |
| `*/15 * * * *` | Every 15 minutes. |
| `30 6 * * *` | 6:30 AM every day. |
| `0 0 1 * *` | Midnight on the first day of every month. |
Expand Down
30 changes: 26 additions & 4 deletions docs/pages/operate/slack-etl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,27 @@ visible to that token. It does not sync DMs or Slackbot-only live thread events.
Private channel rows are protected by RLS: `centaur_readonly` sees public
channel data and the channel in `centaur.slack_channel_id`.

### User-scoped private-channel ingestion

The console's Slack OAuth flow can ingest private channels through the same
user-scoped pipeline used for DMs. Add `groups:read` and `groups:history` to the
Slack OAuth app's allowed user scopes and have existing users consent again.
DM-only credentials continue syncing DMs while credentials with the new scopes
also request `private_channel` conversations.

Every 10 minutes the console worker fans out across healthy Slack broker
credentials. Each credential lists the private channels visible to that user,
reads channel history, and fetches the complete `conversations.members` list.
Messages are deduplicated by Slack conversation and message timestamp when
multiple credentials can see the same channel.

User-scoped private channels are stored with DMs and MPIMs in the neutral
`slack_private_sync_*` and `slack_private_*_context_documents` tables rather than
`company_context_documents`. RLS checks `(team_id, channel_id, user_id)` against
the reconciled membership list. A successful sync marks members omitted from
Slack's complete member list inactive; a partial or truncated member list is
never applied.

## Enable the schedules

Set `apiRs.etl.slack.enabled=true` in Helm values. The chart renders the
Expand Down Expand Up @@ -90,10 +111,11 @@ apiRs:
| `SLACK_RETENTION_ENABLED` | `true` | Allows the `slack_retention` schedule to run when at least one Slack retention TTL is positive. |
| `SLACK_RETENTION_INTERVAL_MINUTES` | `60` | How often to prune Slack retention-managed rows. |
| `SLACK_ETL_RETENTION_DAYS` | `0` | Deletes Slack ETL messages, derived Slack documents, and terminal ETL run/job rows older than this many days. `0` disables ETL retention. |
| `SLACK_DM_RETENTION_DAYS` | `0` | Deletes Slack DM messages, stale empty DM conversations, and terminal DM run/job rows older than this many days. `0` disables DM retention. |
| `SLACK_DM_RETENTION_DAYS` | `0` | Deletes user-scoped private Slack messages, stale empty conversations, and terminal run/job rows older than this many days. `0` disables retention. |
| `COMPANY_CONTEXT_DOCUMENTS_ENABLED` | `true` | Enables projection from Slack sync rows into company context documents. |
| `COMPANY_CONTEXT_DOCUMENTS_INTERVAL_SECONDS` | `14400` | How often to project changed Slack rows into documents. |
| `COMPANY_CONTEXT_DOCUMENTS_MAX_WINDOW_SECONDS` | `21600` | Maximum source `updated_at` window projected by one company context documents run. |
| `COMPANY_CONTEXT_DOCUMENTS_INTERVAL_SECONDS` | `14400` | How often the coordinator claims stale projection scopes. |
| `COMPANY_CONTEXT_DOCUMENTS_MAX_WINDOW_SECONDS` | `21600` | Maximum source `updated_at` window claimed for one scope before it advances its watermark. |
| `COMPANY_CONTEXT_DOCUMENTS_BATCH_SIZE` | `50` | Maximum changed source rows processed by one per-scope child workflow. |

Example exclusion list:

Expand Down Expand Up @@ -278,7 +300,7 @@ setting alerts.
| Channels are all skipped | Check `SLACK_ETL_EXCLUDED_CHANNEL_PATTERNS` for broad globs. |
| Checkpoints show `missing_scope` or `not_allowed_token_type` | Add the missing Slack OAuth scope or use the expected user-token class. |
| Backfill jobs keep failing | Inspect `slack_sync_backfill_jobs.last_error` and the corresponding `slack_sync_runs` row. |
| Documents lag behind messages | Check the `company_context_documents` workflow status and `company_context_projection_lag_seconds`. |
| Documents lag behind messages | Check `company_context_projection_checkpoints` for an expired lease or old watermark, then inspect the per-scope `company_context_documents` child workflow and `company_context_projection_lag_seconds`. |

Keep the ETL token scoped to the channels and workspace data you actually want
agents to retrieve. Synced rows and projected documents are deployment-wide
Expand Down
Loading
Loading