Team Configuration - Z-M-Huang/openhive GitHub Wiki

Team Configuration

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_tools defines what the team can use. Enforcement is via the AI SDK's activeTools array — 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 their mcp__server__tool names, and plugin tools use namespaced keys such as operations.deploy_service or operations.*. Bare plugin names like deploy_service do not match. Glob patterns (e.g., mcp__loggly-mcp__*) are supported.
  • Plugin tool exposure is the intersection of allowed_tools and the active skill's ## Required Tools. A namespaced allowlist entry alone does not load a plugin unless a skill activates it.
  • provider_profile selects from centrally approved profiles. Teams cannot specify raw API URLs or keys.
  • Scope keywords are stored in the SQLite scope_keywords table and serve dual purpose: routing hints for list_teams AND learning domain signal for autonomous learning. Parent agents call list_teams to 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.

Runtime Directory Structure

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 State

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

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.


Rule Cascade

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

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.


Team Initialization

When a team is spawned via spawn_team, it goes through an automatic bootstrap process:

  1. Scaffolding — directories are created, config.yaml is written
  2. Credentials — if provided, stored in team_vault with is_secret = 1
  3. Team context — if provided, written to team-rules/team-context.md (permanent team context: who you are, what you do, what you monitor)
  4. Bootstrap task — auto-queued at critical priority
  5. 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_get tool 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 = 1 in 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

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 = 1 rows are system-managed; teams cannot write or delete them)

Browser Configuration

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.

BrowserRelay 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"]
Loading

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.


Memory System

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.


Data Store Decision Tree

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

Autonomous Learning

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.


window Trigger Configuration

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.


Rate-Limit Buckets

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.

⚠️ **GitHub.com Fallback** ⚠️