Origins Extension
The full normative specification is at spec/hushspec-origins.md.
Overview
The Origins extension provides origin-aware policy projection. When an agent receives work from different sources - Slack channels, GitHub repositories, email threads, Discord servers - different security profiles can apply based on the source context.
Origin profiles narrow the base policy. They can only make rules more restrictive, never more permissive. The base policy remains the security floor.
Origins is declared under extensions.origins in a HushSpec document. When a conformant engine supports the origins extension, incoming requests are matched against origin profiles before rule evaluation begins. The matched profile's constraints are applied as additional restrictions on top of the base policy.
Schema
extensions:
origins:
default_behavior: <"deny"|"minimal_profile"> # OPTIONAL. Default: "deny".
profiles:
- id: <string> # REQUIRED. Unique profile identifier.
match: # OPTIONAL. Matching criteria.
provider: <provider>
tenant_id: <string>
space_id: <string>
space_type: <space_type>
visibility: <visibility>
external_participants: <bool>
tags: [<string>...]
sensitivity: <string>
actor_role: <string>
posture: <state_name> # OPTIONAL. Initial posture state override.
tool_access: # OPTIONAL. Tool access overrides.
allow: [<string>...]
block: [<string>...]
require_confirmation: [<string>...]
default: <"allow"|"block">
max_args_size: <integer>
egress: # OPTIONAL. Egress overrides.
allow: [<string>...]
block: [<string>...]
default: <"allow"|"block">
data: # OPTIONAL. Data handling controls.
allow_external_sharing: <bool>
redact_before_send: <bool>
block_sensitive_outputs: <bool>
budgets: # OPTIONAL. Budget overrides.
tool_calls: <integer>
egress_calls: <integer>
shell_commands: <integer>
bridge: # OPTIONAL. Cross-origin controls.
allow_cross_origin: <bool>
allowed_targets:
- provider: <provider>
space_type: <space_type>
tags: [<string>...]
visibility: <visibility>
require_approval: <bool>
explanation: <string> # OPTIONAL. Why this profile exists.
Field Reference
Top-Level Fields
| Field | Type | Default | Description |
|---|---|---|---|
default_behavior | string | "deny" | What happens when no profile matches. "deny" rejects entirely (fail-closed). "minimal_profile" proceeds under base policy with no origin-specific extensions. |
profiles | array | - | Array of origin profile objects, each with a unique id. |
Match Object Fields
| Field | Type | Description |
|---|---|---|
provider | string | Source provider (e.g. slack, github). |
tenant_id | string | Tenant or workspace identifier. |
space_id | string | Specific channel, room, or repo ID. Highest-priority match. |
space_type | string | Type of space (e.g. channel, pull_request). |
visibility | string | Visibility level (e.g. private, public). |
external_participants | boolean | Whether external users are present in the space. |
tags | array of string | All tags must match (AND semantics). |
sensitivity | string | Sensitivity classification label. |
actor_role | string | Role of the requesting actor. |
Profile Fields
| Field | Type | Description |
|---|---|---|
id | string | REQUIRED. Unique profile identifier. |
match | object | OPTIONAL. Matching criteria. Empty or absent match acts as a default profile. |
posture | string | OPTIONAL. Initial posture state override. Must reference a state in extensions.posture.states. |
tool_access | object | OPTIONAL. Tool access overrides (allow, block, require_confirmation, default, max_args_size). |
egress | object | OPTIONAL. Egress overrides (allow, block, default). |
data | object | OPTIONAL. Data handling controls. |
budgets | object | OPTIONAL. Budget overrides (tool_calls, egress_calls, shell_commands). |
bridge | object | OPTIONAL. Cross-origin data flow controls. |
explanation | string | OPTIONAL. Human-readable reason for why this profile exists. |
Standard Providers
| Provider | Description |
|---|---|
slack | Slack workspace. |
teams | Microsoft Teams. |
github | GitHub (issues, PRs, discussions). |
jira | Atlassian Jira. |
email | Email (any provider). |
discord | Discord server. |
webhook | Generic webhook source. |
custom | Engine-defined provider. |
Engines may support additional providers as strings. Unknown providers should not cause document rejection.
Space Types
| Space Type | Description |
|---|---|
channel | Chat channel (Slack, Teams, Discord). |
group | Group chat or group DM. |
dm | Direct message. |
thread | Threaded conversation. |
issue | Issue tracker entry. |
ticket | Support or service ticket. |
pull_request | Pull or merge request. |
email_thread | Email conversation thread. |
Visibility Levels
| Visibility | Description |
|---|---|
private | Visible only to invited members. |
internal | Visible within the organization. |
public | Visible to anyone. |
external_shared | Shared channel with external participants. |
Match Priority
When an incoming request carries origin context, the engine determines which profile applies using this deterministic priority order:
- Exact
space_idmatch (highest priority). If a profile'smatch.space_idequals the request's space ID, that profile is selected. If multiple profiles match byspace_id, the first in document order wins. - Most specific match by field count. Among remaining profiles, the one with the greatest number of matching
matchfields is selected. Each non-nullmatchfield that equals the corresponding request field counts as one match point.tagscounts as one match point only if all tags in the profile are present in the request. - Provider-only match. A profile matching only on
provideris less specific than one matchingprovider+space_type. - Default profile (empty match). A profile with an empty or absent
matchobject matches all requests at the lowest specificity. At most one default profile should exist. default_behaviorfallback. If no profile matches at all, thedefault_behaviorfield applies.
In case of a tie in match specificity (same number of matching fields, no space_id match), the first profile in document order wins.
Composition Rules
Origin profiles narrow the base policy. The most restrictive rule wins at every level.
Tool Access Composition
| Element | Composition Rule |
|---|---|
| Allowlists | Intersection of base and origin profile. A tool must appear in both to be allowed. |
| Blocklists | Union of base and origin profile. A tool blocked by either is blocked. |
| Require confirmation | Union of both require_confirmation lists. |
| Default | If either the base or origin specifies "block", the effective default is "block". |
| Max args size | The smaller of the two values applies, if both are specified. |
Egress Composition
| Element | Composition Rule |
|---|---|
| Allowlists | Intersection of base and origin profile. |
| Blocklists | Union of base and origin profile. |
| Default | If either specifies "block", the effective default is "block". |
Budget Composition
When both the base posture and the origin profile specify budgets for the same key, the smaller value applies. Origin budgets cannot increase base budgets.
Posture Composition
If an origin profile specifies a posture state, it overrides the base posture initial state for requests from that origin. The referenced state must exist in the posture extension's states map.
Data Policy
The data object controls how content is handled when flowing through or out of the origin context.
| Field | Type | Default | Description |
|---|---|---|---|
allow_external_sharing | boolean | false | Whether content may be shared outside the origin context. |
redact_before_send | boolean | false | Whether sensitive content must be redacted before output. |
block_sensitive_outputs | boolean | false | Whether outputs containing sensitive patterns are blocked entirely. |
Data policy fields default to false (restrictive). The detection of "sensitive content" for redact_before_send and block_sensitive_outputs is governed by the core rules.secret_patterns configuration and any active detection extension. Engines must document their redaction strategy.
Bridge Policy
The bridge object controls whether and how data may flow between origin contexts.
| Field | Type | Default | Description |
|---|---|---|---|
allow_cross_origin | boolean | false | Whether cross-origin data flow is permitted. |
allowed_targets | array of BridgeTarget | [] | Specific targets permitted for cross-origin flow. |
require_approval | boolean | false | Whether cross-origin flow requires user/operator approval. |
Each entry in allowed_targets specifies a permitted destination with optional fields: provider, space_type, tags, visibility. A bridge target matches if all specified fields match; absent fields are wildcards.
Bridge Semantics
- When
allow_cross_originisfalse, no data from this origin context may flow to another origin context. - When
true, data may flow only to destinations matching an entry inallowed_targets. - If
allowed_targetsis empty andallow_cross_originistrue, data may flow to any origin (no target restriction). - If
require_approvalistrue, all cross-origin flows require user/operator approval before proceeding.
Examples
Slack Workspace with Public/Private Channel Profiles
extensions:
origins:
default_behavior: "deny"
profiles:
- id: "slack-private"
match:
provider: slack
space_type: channel
visibility: private
tool_access:
allow: ["read_file", "write_file", "search", "deploy"]
data:
allow_external_sharing: false
explanation: "Full access in private channels"
- id: "slack-public"
match:
provider: slack
space_type: channel
visibility: public
tool_access:
allow: ["read_file", "search"]
block: ["deploy", "write_file"]
data:
allow_external_sharing: false
redact_before_send: true
explanation: "Read-only with redaction in public channels"
- id: "slack-shared"
match:
provider: slack
external_participants: true
tool_access:
allow: ["search"]
data:
redact_before_send: true
block_sensitive_outputs: true
bridge:
allow_cross_origin: false
explanation: "Minimal access in shared channels with external users"
GitHub-Aware Profile for Code Review Bots
extensions:
origins:
default_behavior: "deny"
profiles:
- id: "github-pr"
match:
provider: github
space_type: pull_request
posture: "standard"
tool_access:
allow: ["read_file", "write_file", "search"]
block: ["deploy"]
data:
allow_external_sharing: false
budgets:
tool_calls: 100
explanation: "Code review context - read/write files, no deploy"
- id: "github-issue"
match:
provider: github
space_type: issue
posture: "restricted"
tool_access:
allow: ["read_file", "search"]
budgets:
tool_calls: 30
explanation: "Issue triage - read-only access"
Multi-Provider Setup with Bridge Controls
extensions:
origins:
default_behavior: "deny"
profiles:
- id: "eng-private"
match:
provider: slack
space_type: channel
visibility: private
tags: ["engineering"]
posture: "standard"
tool_access:
allow: ["read_file", "write_file", "search", "deploy"]
egress:
allow: ["api.openai.com", "**.googleapis.com"]
data:
allow_external_sharing: false
redact_before_send: false
bridge:
allow_cross_origin: true
allowed_targets:
- provider: github
space_type: pull_request
require_approval: false
explanation: "Full access for private engineering channels"
- id: "shared-channel"
match:
provider: slack
external_participants: true
posture: "restricted"
tool_access:
allow: ["read_file", "search"]
block: ["deploy"]
egress:
allow: ["api.openai.com"]
data:
allow_external_sharing: false
redact_before_send: true
block_sensitive_outputs: true
budgets:
tool_calls: 20
egress_calls: 10
bridge:
allow_cross_origin: false
explanation: "Restricted access for shared channels with external participants"
- id: "github-pr"
match:
provider: github
space_type: pull_request
posture: "standard"
tool_access:
allow: ["read_file", "write_file", "search"]
data:
allow_external_sharing: false
explanation: "Code review context, no deploy"
Merge Rules
When a child document extends a base document containing origins configuration, the following merge rules apply under deep_merge strategy:
| Element | Merge Behavior |
|---|---|
| Profiles | Child profiles override base profiles by id. If a child defines a profile with the same id as a base profile, the child's profile entirely replaces the base's. New child profiles (with IDs not in the base) are appended. Base profiles whose IDs are not in the child are preserved. |
| Default behavior | If the child defines default_behavior, it overrides the base's value. Otherwise the base value is preserved. |
Under replace strategy, the child's origins object entirely replaces the base's.