Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,519 changes: 1,519 additions & 0 deletions .github/workflows/smoke-enclave-dynamic-issues-read.lock.yml

Large diffs are not rendered by default.

181 changes: 181 additions & 0 deletions .github/workflows/smoke-enclave-dynamic-issues-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
---
description: Smoke test dynamic repository enclave delegation with mcpg v0.4.18+ controller and runtime admission
on:
schedule: every 12h
workflow_dispatch:
permissions:
contents: read
copilot-requests: write
name: Smoke Enclave Dynamic Issues Read
engine:
id: copilot
version: 1.0.80
network:
allowed: []
tools:
github: false
enclaves:
- agent:
model: claude-sonnet-5
max-task-bytes: 8192
max-model-requests: 10
max-model-tokens: 32768
dynamic:
allowed-owners: [github]
sensitivity: internal
Comment on lines +23 to +25
github-policy: github-repository-read-v1
max-repositories: 1
quotas:
max-invocations: 1
max-output-bytes: 1024
max-execution-seconds: 180
audit-labels: [smoke-enclave-dynamic-issues-read]
expires-at: "2027-01-01T00:00:00Z"
memory-limit: 2g
cpu-limit: "2"
pids-limit: 100
tmpfs-limit: 1g
max-invocations: 1
max-output-bytes: 1024
timeout: 180
safe-outputs:
threat-detection:
enabled: false
messages:
footer: "> 🔐📖 *Dynamic Enclave Issues read test by [{workflow_name}]({run_url})*"
run-started: "🔐📖 [{workflow_name}]({run_url}) is testing dynamic read-only GitHub Issues access from an enclave..."
run-success: "🔐📖 [{workflow_name}]({run_url}) completed. Dynamic Enclave Issues read test passed. ✅"
run-failure: "🔐📖 [{workflow_name}]({run_url}) reports {status}. Dynamic Enclave Issues read compatibility issue detected."
timeout-minutes: 20
sandbox:
agent:
id: awf
version: v0.28.14
mcp:
version: v0.4.18
strict: false
concurrency:
group: smoke-enclave-dynamic-issues-read
cancel-in-progress: false
jobs:
verify_enclave:
needs: agent
if: always() && needs.agent.result != 'skipped' && needs.agent.result != 'cancelled'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download agent artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: agent
path: /tmp/gh-aw-agent
- name: Token-usage sanity check
run: node scripts/ci/check-token-usage.js --artifact-root /tmp/gh-aw-agent --engine copilot
post-steps:
- name: Validate dynamic read-only enclave invocation
if: always()
env:
AUDIT_LOG: /tmp/gh-aw/sandbox/firewall/audit/enclave.jsonl
OUTPUTS_FILE: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
run: |
node - "$AUDIT_LOG" "$OUTPUTS_FILE" <<'NODE'
const fs = require("fs");
const [auditPath, outputsPath] = process.argv.slice(2);
const records = fs.readFileSync(auditPath, "utf8")
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line));
const invocations = records.filter((record) =>
record.kind === "invocation" &&
record.repo === "github/gh-aw" &&
record.sensitivity === "internal");
if (invocations.length !== 1) {
throw new Error(`expected one successful enclave invocation, found ${invocations.length}`);
}
const outputs = fs.readFileSync(outputsPath, "utf8")
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line));
const prefix = "ENCLAVE_DYNAMIC_ISSUES_READ_PASS ";
const result = outputs.find((record) =>
record.type === "noop" &&
typeof record.message === "string" &&
record.message.startsWith(prefix));
if (!result) {
throw new Error("agent did not report the dynamic enclave Issues read result through noop");
}
const payload = JSON.parse(result.message.slice(prefix.length));
const expectedKeys = ["comments_read", "issue_number", "issue_read", "list_read"];
if (JSON.stringify(Object.keys(payload).sort()) !== JSON.stringify(expectedKeys) ||
payload.list_read !== true ||
payload.issue_read !== true ||
payload.comments_read !== true ||
!Number.isInteger(payload.issue_number) ||
payload.issue_number <= 0) {
throw new Error("agent reported an invalid dynamic enclave Issues read result");
}
NODE
---

# Smoke Test: Dynamic Read-Only GitHub Issues from an Agent Enclave

The primary agent has no GitHub MCP tools, GitHub credentials, or external
network access. Use `enclave_run_agent` exactly once for the dynamically admitted
`github/gh-aw` repository. Do not attempt GitHub access, network requests, shell
commands, or current-checkout inspection from the primary agent.

Pass this exact finite-disclosure schema:

```json
{
"type": "object",
"fields": {
"list_read": { "type": "boolean" },
"issue_read": { "type": "boolean" },
"comments_read": { "type": "boolean" },
"issue_number": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
```

Give the enclave agent this task:

```text
Use only the `github` MCP server and call each tool exactly once:

1. `list_issues` with `owner: "github"`, `repo: "gh-aw"`, `state: "OPEN"`,
and `perPage: 1`
2. Read the first returned issue number. If no issue is returned, set
`issue_number` to `0` and all booleans to `false`.
3. `issue_read` with `owner: "github"`, `repo: "gh-aw"`, `method: "get"`, and
the discovered issue number.
4. `issue_read` with `owner: "github"`, `repo: "gh-aw"`,
`method: "get_comments"`, the discovered issue number, and `perPage: 1`.

Return exactly one object matching the schema. Set `issue_number` to the number
from the first entry in the `list_issues` response's `issues` array. Set a
boolean to `false` if its corresponding tool call fails, the `issues` field or
comments response is not a JSON array, or the issue response does not contain
the discovered issue number.

Do not use GitHub CLI, GraphQL, search, writes, or any other GitHub tool.
```

The test passes only when all three returned booleans are `true`.

When all booleans are `true` and `issue_number` is positive, call `noop` with
this prefix followed immediately by the enclave result's compact JSON:

```text
ENCLAVE_DYNAMIC_ISSUES_READ_PASS {"list_read":true,"issue_read":true,"comments_read":true,"issue_number":<discovered number>}
```

For any failure, call `safeoutputs missing_data`; never report a failure through
`noop`.
4 changes: 2 additions & 2 deletions .github/workflows/supply-chain-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: github/gh-aw
ref: 1cf55bdc4f19e5f371b1e7fe888df649695ee48a
ref: b52dd75307b4233bd710ce0a0afd96766c2a48e3
path: .tmp/gh-aw-compiler
persist-credentials: false

Expand All @@ -76,7 +76,7 @@ jobs:
mkdir -p "$HOME/.local/share/gh/extensions/gh-aw"
go build \
-C .tmp/gh-aw-compiler \
-ldflags="-X main.version=v0.87.5-108-g1cf55bdc4f -X main.isRelease=true" \
-ldflags="-X main.version=v0.88.6 -X main.isRelease=true" \
-o "$HOME/.local/share/gh/extensions/gh-aw/gh-aw" \
./cmd/gh-aw
rm -rf .tmp/gh-aw-compiler
Expand Down
7 changes: 6 additions & 1 deletion containers/enclave/agent-entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ def redact_diagnostics(value: str) -> str:
redacted,
)
for name, secret in os.environ.items():
if secret and secret != "******" and re.search(r"(?:TOKEN|KEY|SECRET|CREDENTIAL)", name):
if (
secret
and secret != "******"
and secret.lower() not in {"true", "false", "0", "1"}
and re.search(r"(?:TOKEN|KEY|SECRET|CREDENTIAL)", name)
):
Comment on lines +82 to +87
redacted = redacted.replace(secret, "[REDACTED]")
try:
agent_id = GITHUB_AGENT_ID_PATH.read_text(encoding="ascii").strip()
Expand Down
3 changes: 2 additions & 1 deletion scripts/ci/apply-general-workflow-patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export function applyGeneralWorkflowPatches(
// mcpg can rediscover it once AWF attaches and launches the backend.
const isEnclaveSmoke =
workflowPath.endsWith('smoke-enclave-build-test.lock.yml') ||
workflowPath.endsWith('smoke-enclave-issues-read.lock.yml');
workflowPath.endsWith('smoke-enclave-issues-read.lock.yml') ||
workflowPath.endsWith('smoke-enclave-dynamic-issues-read.lock.yml');
if (isEnclaveSmoke) {
const optionalEnclaveServer = '"awf-enclave": {\n "required": false,';
if (content.includes(optionalEnclaveServer)) {
Expand Down
53 changes: 53 additions & 0 deletions scripts/ci/smoke-enclave-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const sourcePath = path.join(workflowsDir, 'smoke-enclave-build-test.md');
const lockPath = path.join(workflowsDir, 'smoke-enclave-build-test.lock.yml');
const issuesSourcePath = path.join(workflowsDir, 'smoke-enclave-issues-read.md');
const issuesLockPath = path.join(workflowsDir, 'smoke-enclave-issues-read.lock.yml');
const dynamicSourcePath = path.join(workflowsDir, 'smoke-enclave-dynamic-issues-read.md');
const dynamicLockPath = path.join(workflowsDir, 'smoke-enclave-dynamic-issues-read.lock.yml');

function countOccurrences(content: string, value: string): number {
return content.split(value).length - 1;
Expand Down Expand Up @@ -180,3 +182,54 @@ describe('smoke enclave issues workflow', () => {
expect(source).toContain('ENCLAVE_ISSUES_READ_PASS');
});
});

describe('smoke enclave dynamic issues workflow', () => {
const source = fs.readFileSync(dynamicSourcePath, 'utf8');
const lock = fs.readFileSync(dynamicLockPath, 'utf8');

it('declares dynamic repository delegation without static repos', () => {
expect(source).toContain('dynamic:\n allowed-owners: [github]');
expect(source).toContain('github-policy: github-repository-read-v1');
expect(source).toContain('sensitivity: internal');
expect(source).not.toContain('repos:\n - repo:');
expect(lock).toContain('\\"dynamic\\":{\\"allowedOwners\\":[\\"github\\"]');
expect(lock).toContain('\\"version\\":\\"github-repository-read-v1\\"');
expect(lock).not.toContain('\\"repos\\":[{\\"repo\\":\\"github/gh-aw\\"');
});

it('uses mcpg v0.4.18 with the delegation controller', () => {
expect(lock).toContain('ghcr.io/github/gh-aw-mcpg:v0.4.18');
expect(lock).toContain('AWF_ENCLAVE_GITHUB_DELEGATION_CONTROL_CAPABILITY=$(openssl rand -hex 32');
expect(lock).toContain('AWF_ENCLAVE_GITHUB_DELEGATION_CONTROL_ENDPOINT="http://127.0.0.1:8090/internal/awf-enclave-mcp-control/github-repository-delegation-v1"');
expect(lock).toContain(
`-p 127.0.0.1:'"\${MCP_GATEWAY_DELEGATION_CONTROL_LISTEN#*:}"':'"\${MCP_GATEWAY_DELEGATION_CONTROL_LISTEN#*:}"'`
);
expect(lock).toContain('Install awf binary (local)');
expect(lock).toContain('--build-local');
});

it('excludes control capability and gateway keys from primary agent', () => {
const executeStep = lock.slice(
lock.indexOf(' - name: Execute GitHub Copilot CLI'),
lock.indexOf(' - name: Detect agent errors')
);
expect(executeStep).toContain('--exclude-env AWF_ENCLAVE_GITHUB_DELEGATION_CONTROL_CAPABILITY');
expect(executeStep).toContain('--exclude-env AWF_ENCLAVE_MCP_CAPABILITY');
expect(executeStep).not.toContain('GH_TOKEN');
expect(executeStep).toContain('--exclude-env MCP_GATEWAY_API_KEY');
});

it('post-processes the dynamic enclave backend idempotently', () => {
const first = applyGeneralWorkflowPatches(lock, dynamicLockPath).content;
const second = applyGeneralWorkflowPatches(first, dynamicLockPath).content;
expect(first).toContain('"awf-enclave": {\n "required": false,');
expect(second).toBe(first);
});

it('validates dynamic read-only enclave audit and execution pass', () => {
expect(source).toContain('ENCLAVE_DYNAMIC_ISSUES_READ_PASS');
expect(source).toContain('"list_read": { "type": "boolean" }');
expect(source).toContain('"issue_read": { "type": "boolean" }');
expect(lock).toContain('ENCLAVE_DYNAMIC_ISSUES_READ_PASS');
});
});
23 changes: 23 additions & 0 deletions src/enclave/agent-entrypoint-diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ if scenario == "github-config":
}))
raise SystemExit(0)

if scenario == "boolean-secrets":
os.environ["BOOLEAN_TOKEN"] = "true"
os.environ["BOOLEAN_KEY"] = "false"
os.environ["BOOLEAN_SECRET"] = "0"
os.environ["BOOLEAN_CREDENTIAL"] = "1"
os.environ["ORDINARY_TOKEN"] = "ordinary-secret"
print(json.dumps({
"exitCode": 0,
"transcript": module.redact_diagnostics(
"true false 0 1 ordinary-secret"
),
"transcriptBytes": 0,
"output": "",
}))
raise SystemExit(0)

if scenario == "bounds":
module.SESSION_LOG_PATH.write_text("", encoding="utf-8")
completed = module.subprocess.CompletedProcess(
Expand Down Expand Up @@ -242,6 +258,13 @@ describe('enclave agent protected entrypoint diagnostics', () => {
expect(result.transcript).toBe('[REDACTED]');
});

it('preserves boolean-like environment values while redacting ordinary secrets', () => {
const result = runHarness('boolean-secrets');

expect(result.transcript).toBe('true false 0 1 [REDACTED]');
expect(result.transcript).not.toContain('ordinary-secret');
});

it('identifies a missing working directory without logging its path', () => {
const result = runHarness('missing-seed');
const transcript = events(result);
Expand Down
Loading
Loading