Team Configuration - Z-M-Huang/openhive GitHub Wiki
Teams are created at runtime via the spawn_team tool — they are not pre-configured on disk. When a team is spawned, its directory structure is scaffolded under .run/teams/{name}/ and its config.yaml is written there.
The team manifest (config.yaml) is written by spawn_team and contains:
| Field | Purpose | Example |
|---|---|---|
name |
Team identifier | operations |
parent |
Parent team in hierarchy | main |
description |
Human-readable team purpose | Monitors logs and system health |
allowed_tools |
Deny-by-default tool allowlist | [read, glob, grep, bash, mcp__loggly-mcp__*, operations.*] |
mcp_servers |
External MCP servers to connect | [loggly-mcp] |
provider_profile |
AI provider profile from providers.yaml
|
default-sonnet |
maxTurns |
Max tool-use steps per session (default: 50) | 50 |
max_concurrent_daily_ops |
Concurrent daily-ops sessions cap per Architecture-Decisions#ADR-41 Daily-ops vs Org-ops Concurrency (default: 5). Org-ops remain single-flight regardless. | 5 |
rate_limit_buckets |
Per-team per-domain token buckets used by web_fetch(..., rate_limit_key). See #Rate-Limit Buckets. |
see below |
Scope keywords are stored separately in the SQLite scope_keywords table, not in config.yaml.
Key constraints:
-
allowed_toolsdefines what the team can use. Enforcement is via the AI SDK'sactiveToolsarray — only tools matching this list are included. Unlisted tools are invisible to the model. Matching is case-sensitive against the real tool key: built-ins use lowercase (read,bash), MCP tools use theirmcp__server__toolnames, and plugin tools use namespaced keys such asoperations.deploy_serviceoroperations.*. Bare plugin names likedeploy_servicedo not match. Glob patterns (e.g.,mcp__loggly-mcp__*) are supported. - Plugin tool exposure is the intersection of
allowed_toolsand the active skill's## Required Tools. A namespaced allowlist entry alone does not load a plugin unless a skill activates it. -
provider_profileselects from centrally approved profiles. Teams cannot specify raw API URLs or keys. - Scope keywords are stored in the SQLite
scope_keywordstable and serve dual purpose: routing hints forlist_teamsAND learning domain signal for autonomous learning. Parent agents calllist_teamsto see each child's keywords, description, and status, then make routing decisions based on the full context. The learning system derives its search topics from these same keywords.
Each team's directory is scaffolded at .run/teams/{name}/ and serves as the agent's working directory. It contains: config.yaml (team manifest, write-protected by governance), org-rules/ (cascading rules), team-rules/ (team-only rules including team-context.md), skills/ (procedure files), plugins/ (team-local TypeScript tool definitions, namespaced as {team_name}.{tool_name}), and subagents/ (agent identity definitions). The exact workspace path is injected into the system prompt.
Plugin tool source and plugin tool lifecycle state are stored separately so deprecation and verification survive restarts:
| Artifact | Location | Purpose |
|---|---|---|
| Source code | .run/teams/{name}/plugins/{tool_name}.ts |
TypeScript tool() implementation imported at runtime |
| Metadata |
.run/openhive.db plugin_tools table |
Persistent lifecycle state (active, deprecated, failed_verification), verification summary, timestamps, source path/hash |
The session loader consults the plugin_tools row before importing the file. Deprecating a tool updates the SQLite row to deprecated but leaves the file on disk for audit and rollback. Removing a tool deletes the file and archives its metadata row (status set to removed, row preserved for audit trail).
Provider profiles are centrally managed in /data/config/providers.yaml:
| Field | Purpose |
|---|---|
type |
Authentication method: api (direct key) or oauth (token-based) |
api_url |
Provider API endpoint (for api type) |
api_key |
API key (for api type). |
model |
Model identifier (e.g., claude-sonnet-4-6-20250514, claude-haiku-4-5-20251001) |
Each profile's provider field determines which AI SDK provider factory is used. Teams select from these approved profiles — they cannot specify raw API URLs or keys.
Rules are markdown files organized in a cascade that flows from global to team-specific. The system concatenates them into the systemPrompt at spawn time. For the full loading order, cascade examples, and rule file format, see Rules-Architecture#Rule Cascade.
Each team has exactly two rule directories:
| Directory | Scope | Cascades? |
|---|---|---|
org-rules/ |
This team and all descendants | Yes — inherited by all sub-teams at any depth |
team-rules/ |
This team only | No |
team-rules/ defines the team's specific identity, persona, and behavioral constraints that should not propagate to children.
Skills and subagents are stored in separate directories under the team root: subagents/ for agent identity definitions (WHO does the work) and skills/ for reusable procedures (HOW to do things). The same skill can be referenced by multiple subagents. The orchestrator always delegates to subagents — it never invokes skills directly (ADR-40). For details, see Skills and Subagents.
Subagent definitions are converted into AI SDK tool() definitions by subagent-factory.ts. Each invocation runs a generateText() call with isolated context — the parent sees only the final result.
When a team is spawned via spawn_team, it goes through an automatic bootstrap process:
- Scaffolding — directories are created, config.yaml is written
-
Credentials — if provided, stored in
team_vaultwithis_secret = 1 -
Team context — if provided, written to
team-rules/team-context.md(permanent team context: who you are, what you do, what you monitor) - Bootstrap task — auto-queued at critical priority
- Self-bootstrap — the team reads its team context (already in prompt via rule cascade) and credentials, then creates its own skills and subagent definitions
The bootstrap task includes instructions for the team to:
- Use
vault_gettool for secrets (team context is already injected via rule cascade) - Create skill files in
skills/for its core tasks - Save initial memory entries via
memory_save(e.g., identity discoveries, initial decisions) - Bootstrap completion is recorded by setting
org_tree.bootstrapped = 1in SQLite
If bootstrap fails, crash recovery resets it to pending for retry. The bootstrapped column on org_tree is checked before re-queuing bootstrap — if already 1, bootstrap is skipped.
Credentials are stored in the team_vault SQLite table with is_secret = 1. They are not injected into the agent's prompt — instead, teams access credentials at runtime via the vault_get tool. This ensures secrets never appear in system prompts or logs. Teams cannot modify their own credentials — rows with is_secret = 1 are system-managed (teams have read-only access via vault_get).
Credential values are automatically:
- Retrieved on demand via
vault_get(key)— never injected into the system prompt - Redacted from logs and agent output by the credential scrubber
- Protected from agent modification (
is_secret = 1rows are system-managed; teams cannot write or delete them)
Teams can be granted browser automation capabilities via the BrowserRelay, which manages a shared @playwright/mcp child process. Add a browser: section to the team's config.yaml to enable browser automation:
| Field | Required | Default | Purpose |
|---|---|---|---|
allowed_domains |
No | All public URLs | Restrict navigable URLs (exact match or glob, e.g., *.example.com) |
timeout_ms |
No | 30000 | Navigation timeout (accepted in schema, not yet consumed) |
Browser tools are gated at two levels: the BrowserRelay must be available (Gate 1), and allowed_tools must include browser tools (Gate 2). Private/reserved IPs are always blocked by SSRF protection. See Browser-Proxy for the full architecture.
flowchart TD
A["browser_navigate handler"] -->|"1. Gate: browser config?<br/>2. Domain allowlist check<br/>3. relay.callTool(...)"| B["BrowserRelay"]
B -->|"@playwright/mcp via stdio<br/>Lazy connect on first use<br/>Idle TTL auto-cleanup<br/>Reconnect on demand"| C["Chromium"]
The relay is initialized at startup as a background task (non-blocking). It spawns the @playwright/mcp CLI as a child process with --headless --no-sandbox and communicates via the MCP stdio transport. After 5 minutes of inactivity, the browser process is automatically closed. The next tool call triggers a reconnect.
Each team has persistent memory stored in the SQLite memories table. Memory entries of type identity, lesson, decision, and context are auto-injected into the system prompt (budget-capped at ~50 entries). Other types (reference, historical) are accessible on demand via the memory_search tool. Memory is per-team isolated via a data access layer and survives session restarts.
For the full memory architecture — search pipeline, embedding configuration, schema, security boundaries, and degradation behavior — see Memory-System.
Teams have three durable stores: vault, memory, and files. Use the following flowchart to decide which store is appropriate for a given piece of data:
| Question | If Yes | Store |
|---|---|---|
| Is this a secret (API key, token, password)? | → | Vault (is_secret=1). Set by spawn_team or admin, read via vault_get. |
| Should this appear in agent reasoning context (prompt or search)? | → | Memory. Use injected types (identity, lesson, decision, context) or search-only types (reference, historical). |
| Is this machine-consumed operational state (config, progress, cache)? | → | Vault (is_secret=0). Read/write via vault_set/vault_get. |
| Is this a file artifact (generated code, reports, exports)? | → | Files in team workspace (.run/teams/{name}/). |
| Use Case | Store | Why |
|---|---|---|
| API keys, tokens | Vault (is_secret=1) |
Never in prompts, scrubbed, team read-only |
| Service config, endpoints | Vault (is_secret=0) |
Operational data, not knowledge |
| Learning progression journal | Vault (is_secret=0) |
Machine-consumed tracking state |
| Learned best practices | Memory (lesson) |
Auto-injected into prompts |
| Team decisions with rationale | Memory (decision) |
Auto-injected into prompts |
| External doc URLs | Memory (reference) |
Searchable knowledge |
| Generated reports, exports | Files | Artifacts, not key-value state |
Learning is controlled entirely through triggers — there is no autonomous_learning config section in config.yaml. The trigger's active/disabled state is the single source of truth. No per-team config fields are needed. System defaults:
| Parameter | Default | Description |
|---|---|---|
max_learnings_per_session |
5 | Cap on new memory entries per cycle |
min_confidence |
medium | Minimum confidence to store a finding |
max_duration_minutes |
30 | Session duration budget; checked before each topic iteration and URL fetch |
Learning topics are derived from the team's scope_keywords — changes to scope keywords automatically affect the next learning cycle without any config update.
For learning cycle phases, see Self-Evolution#Autonomous Learning. For trigger configuration, readiness gates, and management, see Triggers#Learning Trigger.
Continuous-watch duties (e.g., market-hours monitoring) use the window trigger type (ADR-42). The trigger is created via create_trigger(team, name, type: "window", config, ...) — its config is stored in trigger_configs.config as JSON.
| Field | Purpose | Default |
|---|---|---|
watch_window |
Cron expression defining when polling is active (open on entering minute, close on the next non-matching minute) | required |
tick_interval_ms |
Cadence within the open window |
30000 (30 s) |
max_tokens_per_window |
Hard cap on total token consumption per window occurrence | required |
max_ticks_per_window |
Hard cap on number of ticks per window occurrence | required |
overlap_policy |
Reuses existing trigger overlap policy (skip-then-replace, always-skip, always-replace, allow) — applies when a prior tick is still running |
always-skip |
Subagent side: the window-tick subagent's Responsibilities MUST declare cursor keys, no-op return contract, and the idempotency invariant (see Subagents#Window-Trigger Subagents).
For the state machine, overlap semantics, and work-handoff pattern, see Triggers#window Trigger Type. For the LLM-facing "why window ticks feel long-running" explainer, see Tool-Guidelines#Why window ticks feel long-running.
Teams that hit external sources on every tick (common for window triggers scanning news or price feeds) should declare per-domain rate-limit buckets so the web_fetch tool can apply token-bucket throttling deterministically rather than relying on LLM judgement.
rate_limit_buckets is a map of rate_limit_key → bucket config:
| Bucket Field | Purpose |
|---|---|
domain |
Domain or host this bucket applies to (e.g., api.example.com) |
rps |
Refill rate (requests per second) |
burst |
Bucket capacity (max burst size) |
Callers invoke web_fetch(url, method?, headers?, body?, rate_limit_key); the engine looks up the bucket and blocks until a token is available or the request is rejected on cap. A missing or unknown rate_limit_key falls back to the domain allowlist only (no extra throttling). See Organization-Tools#web-fetch-tool.ts.