diff --git a/.changeset/trusted-enclave-sensitivity.md b/.changeset/trusted-enclave-sensitivity.md new file mode 100644 index 00000000000..297e85387c2 --- /dev/null +++ b/.changeset/trusted-enclave-sensitivity.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Add trusted enclave repository sensitivity with strict structured response schemas. diff --git a/.github/aw/enclaves.md b/.github/aw/enclaves.md index 1a236e52ea5..887081e011c 100644 --- a/.github/aw/enclaves.md +++ b/.github/aw/enclaves.md @@ -16,7 +16,8 @@ Use these instructions when a workflow needs bounded, auditable access to a priv ## Prerequisites - Enclaves require AWF network isolation, which every supported `sandbox.agent.runtime` profile provides, so the compiler launches the MCP gateway in bridge mode and AWF can attach it to the isolated topology. -- Each `repos:` entry needs `repo:` (`owner/name`) and `sensitivity:` (`public`, `internal`, `confidential`, or `sealed`). +- Each `repos:` entry needs `repo:` (`owner/name`) and `sensitivity:` (`public`, `trusted`, `internal`, `confidential`, or `sealed`). +- Choose `trusted` only for repositories whose content is approved for unrestricted return to the primary agent without confidentiality accounting, and where the enclave may return free-form strings in a declared response schema. Do not select it merely to obtain string output. All other sensitivities are finite-schema-only; do not recommend free-form string schemas for them. ## Example @@ -66,7 +67,8 @@ enclaves: - `issues-read-v1` permits only the `list_issues` and `issue_read` GitHub MCP tools. GraphQL, search, writes, and all other GitHub tools fail closed. -- V1 allows at most one non-`public` repository in the agent entry. +- V1 allows at most one repository whose sensitivity is neither `public` nor `trusted` in the agent entry; `trusted` is public-equivalent for this limit. +- `trusted` repositories are public-equivalent for this limit, so multiple `trusted` and `public` repositories are allowed. - The compiler generates separate primary and enclave identities for one shared mcpg gateway. The enclave identity is restricted to the GitHub server, those two tools, and the union of repositories declared in its trusted entry. @@ -74,6 +76,23 @@ enclaves: to `/mcp/github`; the enclave has no `gh` executable or GitHub token. - The primary agent receives neither the enclave identity nor the gateway configuration. -- Minimum versions are AWF `v0.28.9` and mcpg `v0.4.15`. +- Minimum versions are AWF `v0.28.9` and mcpg `v0.4.15`; trusted repositories additionally require AWF `v0.28.14`. + +For a trusted repository, an `enclave_run_agent` response schema may contain strings while remaining structured and strict: + +```json +{ + "type": "object", + "fields": { + "should_dispatch": { "type": "boolean" }, + "title": { "type": "string" }, + "problem": { "type": "string" }, + "root_cause": { "type": "string" }, + "proposed_solution": { "type": "string" } + } +} +``` + +Responses must conform exactly to the declared schema: fields are required, extra properties are rejected, floats, `$ref`, recursion, regex schemas, and untagged unions are unsupported. Output remains subject to AWF's configured limit and the global 8 KiB ceiling. See also: [agent-runtime-instructions.md](agent-runtime-instructions.md) for `sandbox.agent` fields, and [network.md](network.md) for network isolation defaults. diff --git a/docs/src/content/docs/experimental/enclaves.md b/docs/src/content/docs/experimental/enclaves.md index cadafacea53..30e2dbdf888 100644 --- a/docs/src/content/docs/experimental/enclaves.md +++ b/docs/src/content/docs/experimental/enclaves.md @@ -25,7 +25,7 @@ enclaves: timeout: 180 ``` -Each type can appear at most once. When the same repository appears in both entries, its sensitivity must match because its information budget is shared across executor types. AWF fixes the script enclave network and interpreter and the agent enclave network internally; workflows cannot override those security invariants. +Each `repos` entry uses `public`, `trusted`, `internal`, `confidential`, or `sealed` sensitivity. `trusted` is unmetered and permits free-form string values inside an otherwise strict structured response schema, but is appropriate only when repository content is approved for unrestricted return to the primary agent without confidentiality accounting. Do not select it merely to obtain string output. All other sensitivities are finite-schema-only. Each type can appear at most once. When the same repository appears in both entries, its sensitivity must match because its information budget is shared across executor types. AWF fixes the script enclave network and interpreter and the agent enclave network internally; workflows cannot override those security invariants. The generated gateway upstream uses a fresh masked capability for each workflow run. That capability is passed only to mcpg and AWF and is excluded from the primary agent environment. The gateway allows 120 seconds for the AWF-owned HTTP upstream to become available. It enforces a 4,860-second tool timeout, covering AWF's maximum 4,800-second finite-disclosure timing bucket plus a 60-second transport allowance. Executor timeouts are capped at 4,740 seconds because AWF reserves 60 seconds in the final bucket for processing and cleanup. The gateway timeout is an enforcement bound, not an absolute AWF wall-clock guarantee under pathological host cleanup or scheduler stalls. @@ -53,9 +53,7 @@ enclaves: ``` `issues-read-v1` is the only accepted `agent.github.cli` value. Script -enclaves cannot configure `github`. The first profile version accepts at most -one repository whose sensitivity is not `public`; additional assigned -repositories must declare `sensitivity: public`. +enclaves cannot configure `github`. The first profile version accepts at most one repository whose sensitivity is neither `public` nor `trusted`; additional assigned repositories may declare `sensitivity: public` or `trusted`. The profile permits only `list_issues` and `issue_read` through the GitHub MCP server. GraphQL, search, writes, and every other GitHub tool are denied. @@ -72,5 +70,5 @@ to the assigned repository's Issues. The fallback `GITHUB_TOKEN` can only access repositories that token can already read (typically just the current repository in Actions). -The minimum supported versions are AWF `v0.28.9` and mcpg `v0.4.15`. The +The minimum supported versions are AWF `v0.28.9` (or `v0.28.14` when using `trusted`) and mcpg `v0.4.15`. The compiler does not fall back to older versions. diff --git a/docs/src/content/docs/reference/glossary.md b/docs/src/content/docs/reference/glossary.md index 0004ce45a47..c60324c213c 100644 --- a/docs/src/content/docs/reference/glossary.md +++ b/docs/src/content/docs/reference/glossary.md @@ -161,7 +161,7 @@ A built-in tool that connects agentic workflows to Atlassian's official remote R ### Enclaves (`enclaves:`) -A top-level frontmatter array that enables finite-disclosure access to approved private repositories from within a public-facing workflow. The compiler registers `enclave_run_script` or `enclave_run_agent` tools from the keyed `script`/`agent` entries present on the `awf-enclave` MCP route, compiled through [mcpg](#mcp-gateway) with run-scoped capability handoff, timeout derivation, and network validation. Enclaves require AWF network isolation, which every supported `sandbox.agent.runtime` profile provides, so the compiler can launch mcpg in bridge mode. Agent enclaves may opt into `github.cli: issues-read-v1`, which uses a distinct identity on the shared compiler-owned mcpg gateway and permits only `list_issues` and `issue_read` for the configured trusted repositories. AWF privately stages this identity and connects the enclave directly to `/mcp/github`; the enclave receives neither a GitHub token nor a `gh` executable. See [Private Repository Enclaves](/gh-aw/experimental/enclaves/). +A top-level frontmatter array that enables finite-disclosure access to approved repositories from within a public-facing workflow. Repository sensitivities are `public`, `trusted`, `internal`, `confidential`, or `sealed`; `trusted` is unmetered and permits free-form strings only inside a declared strict structured response schema, while the other sensitivities remain finite-schema-only. The compiler registers `enclave_run_script` or `enclave_run_agent` tools from the keyed `script`/`agent` entries present on the `awf-enclave` MCP route, compiled through [mcpg](#mcp-gateway) with run-scoped capability handoff, timeout derivation, and network validation. Enclaves require AWF network isolation, which every supported `sandbox.agent.runtime` profile provides, so the compiler can launch mcpg in bridge mode. Agent enclaves may opt into `github.cli: issues-read-v1`, which uses a distinct identity on the shared compiler-owned mcpg gateway and permits only `list_issues` and `issue_read` for the configured trusted repositories. AWF privately stages this identity and connects the enclave directly to `/mcp/github`; the enclave receives neither a GitHub token nor a `gh` executable. See [Private Repository Enclaves](/gh-aw/experimental/enclaves/). ### MCP Scripts diff --git a/pkg/constants/version_constants.go b/pkg/constants/version_constants.go index 3cf9e45cf56..3a14c455f38 100644 --- a/pkg/constants/version_constants.go +++ b/pkg/constants/version_constants.go @@ -158,6 +158,10 @@ const AWFCloudHypervisorFilesystemAllowWriteMinVersion Version = "v0.28.6" // config schema accepts enclaves[].agent.github.cli = "issues-read-v1". const AWFEnclaveGitHubIssuesMinVersion Version = "v0.28.9" +// AWFEnclaveTrustedSensitivityMinVersion is the first AWF version whose +// enclave response schema permits free-form string values for trusted repositories. +const AWFEnclaveTrustedSensitivityMinVersion Version = "v0.28.14" + // AWFAPIProxyCACertMinVersion is the minimum AWF version that supports // apiProxy.caCert in awf-config.json (mapped from frontmatter // sandbox.agent.ca-cert). Older AWF versions reject the unknown property diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 631c72194b7..8e8b7158f91 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -202,6 +202,15 @@ func TestValidateMainWorkflowFrontmatterEnclaves(t *testing.T) { t.Fatalf("expected keyed top-level enclaves to validate: %v", err) } + valid["enclaves"].([]any)[0].(map[string]any)["repos"].([]any)[0].(map[string]any)["sensitivity"] = "trusted" + if err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(valid, "workflow.md"); err != nil { + t.Fatalf("expected trusted enclave sensitivity to validate: %v", err) + } + valid["enclaves"].([]any)[0].(map[string]any)["repos"].([]any)[0].(map[string]any)["sensitivity"] = "unsupported" + if err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(valid, "workflow.md"); err == nil { + t.Fatal("expected unsupported enclave sensitivity to be rejected") + } + legacy := map[string]any{ "on": "workflow_dispatch", "engine": "copilot", diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 33f1f7e2066..9a1a32bc45e 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -13763,7 +13763,7 @@ }, "sensitivity": { "type": "string", - "enum": ["public", "internal", "confidential", "sealed"] + "enum": ["public", "trusted", "internal", "confidential", "sealed"] } } } diff --git a/pkg/workflow/enclave_github_proxy_test.go b/pkg/workflow/enclave_github_proxy_test.go index f43b9db5788..65fe33da227 100644 --- a/pkg/workflow/enclave_github_proxy_test.go +++ b/pkg/workflow/enclave_github_proxy_test.go @@ -15,14 +15,16 @@ import ( func TestEnclaveGitHubMCPAgentPolicy(t *testing.T) { data := enclaveGitHubIssuesWorkflowData() - data.Enclaves[0].Repos = append(data.Enclaves[0].Repos, - &EnclaveRepository{Repo: "octo-org/public-docs", Sensitivity: "public"}) + data.Enclaves[0].Repos = []*EnclaveRepository{ + {Repo: "octo-org/trusted-service", Sensitivity: "trusted"}, + {Repo: "octo-org/public-docs", Sensitivity: "public"}, + } policy := enclaveGitHubMCPAgentPolicy(data) assert.Equal(t, []string{"github"}, policy.Servers) assert.Equal(t, map[string][]string{"github": {"list_issues", "issue_read"}}, policy.Tools) assert.Equal(t, map[string]any{ - "repos": []string{"octo-org/private-service", "octo-org/public-docs"}, + "repos": []string{"octo-org/trusted-service", "octo-org/public-docs"}, "min-integrity": "approved", }, policy.AllowOnly) } diff --git a/pkg/workflow/enclaves.go b/pkg/workflow/enclaves.go index 53d17c73e5f..14db954cd4a 100644 --- a/pkg/workflow/enclaves.go +++ b/pkg/workflow/enclaves.go @@ -175,6 +175,9 @@ func validateEnclavesConfig(workflowData *WorkflowData) error { return err } } + if err := validateEnclaveTrustedSensitivityVersion(workflowData); err != nil { + return err + } if err := validateEnclaveGitHubIssuesVersions(workflowData); err != nil { return err } @@ -229,11 +232,11 @@ func validateEnclaveRepositories(index int, enclave *EnclaveConfig, repositorySe } seenInEnclave[key] = struct{}{} switch repo.Sensitivity { - case "public", "internal", "confidential", "sealed": + case "public", "trusted", "internal", "confidential", "sealed": default: - return 0, fmt.Errorf("enclaves[%d].repos[%d].sensitivity must be public, internal, confidential, or sealed. Example:\n\nenclaves:\n - script:\n repos:\n - repo: org/my-repo\n sensitivity: confidential", index, repoIndex) + return 0, fmt.Errorf("enclaves[%d].repos[%d].sensitivity must be public, trusted, internal, confidential, or sealed. Example:\n\nenclaves:\n - script:\n repos:\n - repo: org/my-repo\n sensitivity: confidential", index, repoIndex) } - if repo.Sensitivity != "public" { + if repo.Sensitivity != "public" && repo.Sensitivity != "trusted" { nonPublicRepositories++ } if sensitivity, ok := repositorySensitivities[key]; ok && sensitivity != repo.Sensitivity { @@ -270,6 +273,28 @@ func validateEnclaveGitHubIssuesVersions(workflowData *WorkflowData) error { return nil } +func validateEnclaveTrustedSensitivityVersion(workflowData *WorkflowData) error { + for _, enclave := range workflowData.Enclaves { + if enclave == nil { + continue + } + for _, repo := range enclave.Repos { + if repo != nil && repo.Sensitivity == "trusted" { + firewallConfig := getFirewallConfig(workflowData) + if !awfVersionAtLeast(firewallConfig, constants.AWFEnclaveTrustedSensitivityMinVersion) { + effectiveVersion := string(constants.DefaultFirewallVersion) + if firewallConfig != nil && firewallConfig.Version != "" { + effectiveVersion = firewallConfig.Version + } + return fmt.Errorf("enclaves[].repos sensitivity %q requires AWF %s or newer, but the effective version is %s", "trusted", constants.AWFEnclaveTrustedSensitivityMinVersion, effectiveVersion) + } + return nil + } + } + } + return nil +} + func enclaveExecutor(enclave *EnclaveConfig) (string, bool) { if enclave.Script != nil && enclave.Agent == nil { return "script", true diff --git a/pkg/workflow/enclaves_test.go b/pkg/workflow/enclaves_test.go index e484950082c..b1067adbd55 100644 --- a/pkg/workflow/enclaves_test.go +++ b/pkg/workflow/enclaves_test.go @@ -202,6 +202,24 @@ func TestValidateEnclaveGitHubIssuesRepositoryLimit(t *testing.T) { require.NoError(t, validateEnclavesConfig(data)) } +func TestValidateEnclaveGitHubIssuesRepositoryLimitTreatsTrustedAsPublic(t *testing.T) { + data := enclaveGitHubIssuesWorkflowData() + data.Enclaves[0].Repos = []*EnclaveRepository{ + {Repo: "octo-org/trusted-service", Sensitivity: "trusted"}, + {Repo: "octo-org/public-service", Sensitivity: "public"}, + } + require.NoError(t, validateEnclavesConfig(data)) +} + +func TestValidateEnclaveTrustedSensitivityRequiresAWFVersion(t *testing.T) { + data := enclaveWorkflowData(false, true, 0, 120) + data.Enclaves[0].Repos[0].Sensitivity = "trusted" + data.NetworkPermissions.Firewall.Version = "v0.28.12" + err := validateEnclavesConfig(data) + require.Error(t, err) + assert.Contains(t, err.Error(), "requires AWF v0.28.14 or newer") +} + func TestValidateEnclaveGitHubIssuesRepositoryLimitScopesToGitHubEntry(t *testing.T) { data := enclaveWorkflowData(true, true, 30, 120) data.Enclaves[0].Repos = []*EnclaveRepository{{ diff --git a/pkg/workflow/schemas/awf-config.schema.json b/pkg/workflow/schemas/awf-config.schema.json index 7baf3881c8c..685a69163d6 100644 --- a/pkg/workflow/schemas/awf-config.schema.json +++ b/pkg/workflow/schemas/awf-config.schema.json @@ -953,7 +953,7 @@ }, "sensitivity": { "type": "string", - "enum": ["public", "internal", "confidential", "sealed"] + "enum": ["public", "trusted", "internal", "confidential", "sealed"] } } } @@ -1081,7 +1081,7 @@ }, "sensitivity": { "type": "string", - "enum": ["public", "internal", "confidential", "sealed"] + "enum": ["public", "trusted", "internal", "confidential", "sealed"] } } } @@ -1152,7 +1152,7 @@ }, "privateRepos": { "type": "array", - "description": "Private repositories a bounded agent may reason about, each with a trusted confidentiality category that fixes its per-run information budget (`public` unmetered, `internal` 64 bits/run, `confidential` 8 bits/run, `sealed` 0 bits/run — never launches an enclave). Each `repo` must be a bare `owner/repo` slug — no scheme, host, credentials, path traversal, query string, fragment, or wildcard. Repository names must be unique case-insensitively. Bounded agents keep a ledger separate from bounded queries.", + "description": "Repositories a bounded agent may reason about, each with a trusted confidentiality category that fixes its per-run information budget (`public` and `trusted` unmetered, `internal` 64 bits/run, `confidential` 8 bits/run, `sealed` 0 bits/run — never launches an enclave). Trusted enclave responses may contain free-form strings only inside strict structured schemas. Each `repo` must be a bare `owner/repo` slug — no scheme, host, credentials, path traversal, query string, fragment, or wildcard. Repository names must be unique case-insensitively. Bounded agents keep a ledger separate from bounded queries.", "items": { "type": "object", "additionalProperties": false, @@ -1165,7 +1165,7 @@ }, "sensitivity": { "type": "string", - "enum": ["public", "internal", "confidential", "sealed"], + "enum": ["public", "trusted", "internal", "confidential", "sealed"], "description": "Confidentiality category, which fixes this repository's immutable per-run information budget. Cannot be increased by configuration." } } diff --git a/schema-demos/schema-demo-enclaves.md b/schema-demos/schema-demo-enclaves.md index 2b2930830c9..2ec5a37441f 100644 --- a/schema-demos/schema-demo-enclaves.md +++ b/schema-demos/schema-demo-enclaves.md @@ -15,14 +15,14 @@ enclaves: - script: repos: - repo: octo-org/private-service - sensitivity: confidential + sensitivity: trusted - agent: model: gpt-5 github: cli: issues-read-v1 repos: - repo: octo-org/private-service - sensitivity: confidential + sensitivity: trusted timeout-minutes: 5 ---