Skip to content

Commit 8f964e0

Browse files
authored
Guard DIFC: add explicit label rules for notification writes and repository create/fork operations (#4300)
The guard’s write classification was complete, but several mutating GitHub MCP tools could still inherit caller-provided DIFC labels via fallback behavior. This change removes that ambiguity for high-impact write paths by defining explicit secrecy/integrity outcomes. - **Notification write operations now have explicit DIFC semantics** - Added dedicated `apply_tool_labels` handling for: - `dismiss_notification` - `mark_all_notifications_read` - `manage_notification_subscription` - `manage_repository_notification_subscription` - These now resolve to: - `S = public` (`[]`) - `I = project:github` - baseline scope `github` - **Repository creation/fork operations split from repo-content writes** - Moved `create_repository` and `fork_repository` out of repo-content mutation grouping. - Added explicit account-scoped rule: - `S = public` (`[]`) - `I = writer(github)` - baseline scope `github` - **Coverage updates in label-rule tests** - Replaced single-tool notification assertion with grouped assertions for all notification management write tools. - Updated `fork_repository` expectations to github-scoped writer integrity/public secrecy. - Added `create_repository` test to lock intended DIFC behavior. ```rust // Notification management (explicit write semantics) "dismiss_notification" | "mark_all_notifications_read" | "manage_notification_subscription" | "manage_repository_notification_subscription" => { secrecy = vec![]; baseline_scope = "github".to_string(); integrity = project_github_label(ctx); } // Repo creation/fork (account-scoped writes) "create_repository" | "fork_repository" => { secrecy = vec![]; baseline_scope = "github".to_string(); integrity = writer_integrity("github", ctx); } ``` > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `example.com` > - Triggering command: `/tmp/go-build1964592199/b509/launcher.test /tmp/go-build1964592199/b509/launcher.test -test.testlogfile=/tmp/go-build1964592199/b509/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ache/go/1.25.9/x-p go x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build2556502756/b513/launcher.test /tmp/go-build2556502756/b513/launcher.test -test.testlogfile=/tmp/go-build2556502756/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� .rlib 6b2b26a06.rlib 327.rlib 653.rlib z8tylr32cgwnziux/tmp/go-build197148868/b453/vet.cfg ruczucboywd9kbz6.0ufrg49.rcgu.o aiea5rg1toc6oekg.0ufrg49.rcgu.o fnr5�� p1agk9if6se1ud54.0ufrg49.rcgu.o fc88gtczrpthgg1u.0ufrg49.rcgu.o x_amd64/vet 5vmlrtxer9j2lnp9/opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linux_amd64/link zucrirrh7hnf4z1l-o o4qlefxtp7qruodt/tmp/go-build2556502756/b507/guard.test x_amd64/vet` (dns block) > - `invalid-host-that-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build1964592199/b491/config.test /tmp/go-build1964592199/b491/config.test -test.testlogfile=/tmp/go-build1964592199/b491/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/c-p go x_amd64/compile` (dns block) > - Triggering command: `/tmp/go-build2556502756/b495/config.test /tmp/go-build2556502756/b495/config.test -test.testlogfile=/tmp/go-build2556502756/b495/testlog.txt -test.paniconexit0 -test.timeout=10m0s -o ithub-guard/rust-guard/target/debug/build/serde-0c0d3ac83fe3437f/rustcMFMlVN/symbols.o ithub-guard/rust-guard/target/debug/build/serde-0c0d3ac83fe3437f/build_script_build-0c0d3ac83fe3/tmp/go-build197148868/b384/vet.cfg ithub-guard/rust-guard/target/debug/build/serde-0c0d3ac83fe3437f/build_script_build-0c0d3ac83fe3437f.build_scrip/tmp/go-build1020409741/b001/exe/a.out ithub-guard/rust/opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linux_amd64/vet ild-e2b702800175/tmp/go-build197148868/b292/vet.cfg ild-e2b702800175437e.f3itfh70g07-m known-linux-gnu/lib/rustlib/x86_fix(guard): enforce explicit DIFC labels for notification management and repo cr-buildid=dcc-Xk-BVFiljIesDi3n/H9wQikqwjRQnx90KP6dH/hKNTvz9caF-1eL6QKuz0/dcc-Xk-B-unsafeptr=false know�� known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libobject-926daa94a00ee327.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libmemchr-48d5b0db80402653.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libaddr2line-3367f26bd486b29d.rlib known-linux-gnu//opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linux_amd64/vet known-linux-gnu//tmp/go-build197148868/b425/vet.cfg known-linux-gnu/lib/rustlib/x86_/tmp/go-build197148868/b039/vet.cfg known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libstd_detect-b16e5cb5eba3e0fd.rlib` (dns block) > - `nonexistent.local` > - Triggering command: `/tmp/go-build1964592199/b509/launcher.test /tmp/go-build1964592199/b509/launcher.test -test.testlogfile=/tmp/go-build1964592199/b509/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ache/go/1.25.9/x-p go x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build2556502756/b513/launcher.test /tmp/go-build2556502756/b513/launcher.test -test.testlogfile=/tmp/go-build2556502756/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� .rlib 6b2b26a06.rlib 327.rlib 653.rlib z8tylr32cgwnziux/tmp/go-build197148868/b453/vet.cfg ruczucboywd9kbz6.0ufrg49.rcgu.o aiea5rg1toc6oekg.0ufrg49.rcgu.o fnr5�� p1agk9if6se1ud54.0ufrg49.rcgu.o fc88gtczrpthgg1u.0ufrg49.rcgu.o x_amd64/vet 5vmlrtxer9j2lnp9/opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linux_amd64/link zucrirrh7hnf4z1l-o o4qlefxtp7qruodt/tmp/go-build2556502756/b507/guard.test x_amd64/vet` (dns block) > - `slow.example.com` > - Triggering command: `/tmp/go-build1964592199/b509/launcher.test /tmp/go-build1964592199/b509/launcher.test -test.testlogfile=/tmp/go-build1964592199/b509/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ache/go/1.25.9/x-p go x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build2556502756/b513/launcher.test /tmp/go-build2556502756/b513/launcher.test -test.testlogfile=/tmp/go-build2556502756/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� .rlib 6b2b26a06.rlib 327.rlib 653.rlib z8tylr32cgwnziux/tmp/go-build197148868/b453/vet.cfg ruczucboywd9kbz6.0ufrg49.rcgu.o aiea5rg1toc6oekg.0ufrg49.rcgu.o fnr5�� p1agk9if6se1ud54.0ufrg49.rcgu.o fc88gtczrpthgg1u.0ufrg49.rcgu.o x_amd64/vet 5vmlrtxer9j2lnp9/opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linux_amd64/link zucrirrh7hnf4z1l-o o4qlefxtp7qruodt/tmp/go-build2556502756/b507/guard.test x_amd64/vet` (dns block) > - `this-host-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build1964592199/b518/mcp.test /tmp/go-build1964592199/b518/mcp.test -test.testlogfile=/tmp/go-build1964592199/b518/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true proto/proto.go s/alert.go x_amd64/compile -o lts /tmp/cc9cHcxs.s x_amd64/compile -I g_.a 4592199/b165/ x_amd64/vet --gdwarf-5 ternal/sys -o x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build2556502756/b522/mcp.test /tmp/go-build2556502756/b522/mcp.test -test.testlogfile=/tmp/go-build2556502756/b522/testlog.txt -test.paniconexit0 -test.timeout=10m0s ithu�� ithub-guard/rust-guard/target/debug/deps/github_guard-57d41235e07a5585.3r7b32ab9hw1mmy6a1aekmmsh/usr/libexec/docker/docker-init ithub-guard/rust-guard/target/debug/deps/github_guard-57d41235e07a5585.485oswk5h4p2kq4dabhq5b2ke--version x_amd64/vet 6e1dc71b.rlib m/github/gh-aw-m--version -nilfunc x_amd64/vet n-me�� aw-mcpg/guards/github-guard/rust-guard/target/debug/deps/rustcq8hDXS/symbols.o aw-mcpg/guards/github-guard/rust-guard/target/debug/deps/github_guard-57d41235e07a5585.0r6f2y9pm/usr/bin/runc x_amd64/vet aw-mcpg/guards/gbash aw-mcpg/guards/g/usr/bin/runc aw-mcpg/guards/g--version x_amd64/vet` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent) (admins only) > > </details>
2 parents 1c41e8a + c2b28dd commit 8f964e0

4 files changed

Lines changed: 197 additions & 45 deletions

File tree

guards/github-guard/rust-guard/src/labels/mod.rs

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4614,22 +4614,31 @@ mod tests {
46144614
}
46154615

46164616
#[test]
4617-
fn test_apply_tool_labels_dismiss_notification_private_user() {
4617+
fn test_apply_tool_labels_notification_management_public_project_github() {
46184618
let ctx = default_ctx();
46194619
let tool_args = json!({ "threadId": "123" });
46204620

4621-
let (secrecy, integrity, _desc) = apply_tool_labels(
4621+
for tool in &[
46224622
"dismiss_notification",
4623-
&tool_args,
4624-
"",
4625-
vec![],
4626-
vec![],
4627-
String::new(),
4628-
&ctx,
4629-
);
4623+
"mark_all_notifications_read",
4624+
"manage_notification_subscription",
4625+
"manage_repository_notification_subscription",
4626+
] {
4627+
let (secrecy, integrity, _desc) =
4628+
apply_tool_labels(tool, &tool_args, "", vec![], vec![], String::new(), &ctx);
46304629

4631-
assert_eq!(secrecy, private_user_label(), "dismiss_notification must carry private:user secrecy");
4632-
assert_eq!(integrity, none_integrity("", &ctx), "dismiss_notification should have none-level integrity (references unknown content)");
4630+
assert!(
4631+
secrecy.is_empty(),
4632+
"{} should have empty (public) secrecy",
4633+
tool
4634+
);
4635+
assert_eq!(
4636+
integrity,
4637+
project_github_label(&ctx),
4638+
"{} should have project:github integrity",
4639+
tool
4640+
);
4641+
}
46334642
}
46344643

46354644
#[test]
@@ -4919,7 +4928,7 @@ mod tests {
49194928
"repo": "copilot"
49204929
});
49214930

4922-
let (_secrecy, integrity, _desc) = apply_tool_labels(
4931+
let (secrecy, integrity, _desc) = apply_tool_labels(
49234932
"fork_repository",
49244933
&tool_args,
49254934
repo_id,
@@ -4929,7 +4938,38 @@ mod tests {
49294938
&ctx,
49304939
);
49314940

4932-
assert_eq!(integrity, writer_integrity(repo_id, &ctx), "fork_repository should have writer integrity");
4941+
assert!(secrecy.is_empty(), "fork_repository should have empty (public) secrecy");
4942+
assert_eq!(
4943+
integrity,
4944+
writer_integrity("github", &ctx),
4945+
"fork_repository should have writer integrity in github scope"
4946+
);
4947+
}
4948+
4949+
#[test]
4950+
fn test_apply_tool_labels_create_repository_writer_integrity() {
4951+
let ctx = default_ctx();
4952+
let repo_id = "github/copilot";
4953+
let tool_args = json!({
4954+
"name": "new-repo"
4955+
});
4956+
4957+
let (secrecy, integrity, _desc) = apply_tool_labels(
4958+
"create_repository",
4959+
&tool_args,
4960+
repo_id,
4961+
vec![],
4962+
vec![],
4963+
String::new(),
4964+
&ctx,
4965+
);
4966+
4967+
assert!(secrecy.is_empty(), "create_repository should have empty (public) secrecy");
4968+
assert_eq!(
4969+
integrity,
4970+
writer_integrity("github", &ctx),
4971+
"create_repository should have writer integrity in github scope"
4972+
);
49334973
}
49344974

49354975
#[test]

guards/github-guard/rust-guard/src/labels/tool_rules.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -466,17 +466,26 @@ pub fn apply_tool_labels(
466466
}
467467

468468
// === Notifications (user-scoped, private) ===
469-
"list_notifications" | "get_notification_details"
470-
| "dismiss_notification" | "mark_all_notifications_read"
471-
| "manage_notification_subscription"
472-
| "manage_repository_notification_subscription" => {
469+
"list_notifications" | "get_notification_details" => {
473470
// Notifications are private to the authenticated user.
474471
// S = private:user
475472
// I = none (notifications reference external content of unknown trust)
476473
secrecy = private_user_label();
477474
integrity = vec![];
478475
}
479476

477+
// === Notification management (account-scoped writes) ===
478+
"dismiss_notification"
479+
| "mark_all_notifications_read"
480+
| "manage_notification_subscription"
481+
| "manage_repository_notification_subscription" => {
482+
// These operations change notification/subscription state and return minimal metadata.
483+
// S = public (empty); I = project:github
484+
secrecy = vec![];
485+
baseline_scope = "github".to_string();
486+
integrity = project_github_label(ctx);
487+
}
488+
480489
// === Private GitHub-controlled metadata (user-associated): PII/org-structure sensitive ===
481490
"get_me"
482491
| "get_teams"
@@ -562,14 +571,23 @@ pub fn apply_tool_labels(
562571

563572
// === Repo content and structure write operations ===
564573
"create_or_update_file" | "push_files" | "delete_file" | "create_branch"
565-
| "update_pull_request_branch" | "create_repository" | "fork_repository" => {
574+
| "update_pull_request_branch" => {
566575
// Write operations that modify repo content/structure.
567576
// S = S(repo) — response references repo-scoped content
568577
// I = writer (agent-authored content)
569578
secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx);
570579
integrity = writer_integrity(repo_id, ctx);
571580
}
572581

582+
// === Repository creation/fork (user/org-scoped writes) ===
583+
"create_repository" | "fork_repository" => {
584+
// Creating/forking repositories is account-scoped and does not return repo content.
585+
// S = public (empty); I = writer(github)
586+
secrecy = vec![];
587+
baseline_scope = "github".to_string();
588+
integrity = writer_integrity("github", ctx);
589+
}
590+
573591
// === Projects write operations (org-scoped) ===
574592
"projects_write"
575593
// Deprecated aliases that map to projects_write

guards/github-guard/rust-guard/src/lib.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,12 @@ fn infer_scope_for_baseline(tool_name: &str, tool_args: &Value, repo_id: &str) -
262262
}
263263

264264
match tool_name {
265+
"dismiss_notification"
266+
| "mark_all_notifications_read"
267+
| "manage_notification_subscription"
268+
| "manage_repository_notification_subscription"
269+
| "create_repository"
270+
| "fork_repository" => "github".to_string(),
265271
"search_code" | "search_issues" | "search_pull_requests" => {
266272
let query = tool_args
267273
.get("query")
@@ -1159,6 +1165,74 @@ mod tests {
11591165
assert_eq!(inferred, "github/gh-aw-mcpg");
11601166
}
11611167

1168+
#[test]
1169+
fn infer_scope_for_baseline_uses_github_scope_for_notification_management_tools() {
1170+
let tool_args = json!({ "threadId": "123" });
1171+
for tool in &[
1172+
"dismiss_notification",
1173+
"mark_all_notifications_read",
1174+
"manage_notification_subscription",
1175+
"manage_repository_notification_subscription",
1176+
] {
1177+
let inferred = infer_scope_for_baseline(tool, &tool_args, "");
1178+
assert_eq!(inferred, "github", "{} should infer github baseline scope", tool);
1179+
}
1180+
}
1181+
1182+
#[test]
1183+
fn infer_scope_for_baseline_uses_github_scope_for_repo_creation_tools() {
1184+
let tool_args = json!({ "name": "new-repo" });
1185+
for tool in &["create_repository", "fork_repository"] {
1186+
let inferred = infer_scope_for_baseline(tool, &tool_args, "");
1187+
assert_eq!(inferred, "github", "{} should infer github baseline scope", tool);
1188+
}
1189+
}
1190+
1191+
#[test]
1192+
fn notification_management_integrity_preserved_after_baseline() {
1193+
let ctx = PolicyContext::default();
1194+
let tool_args = json!({ "threadId": "123" });
1195+
for tool in &[
1196+
"dismiss_notification",
1197+
"mark_all_notifications_read",
1198+
"manage_notification_subscription",
1199+
"manage_repository_notification_subscription",
1200+
] {
1201+
let (_, integrity, _) =
1202+
labels::apply_tool_labels(tool, &tool_args, "", vec![], vec![], String::new(), &ctx);
1203+
let baseline_scope = infer_scope_for_baseline(tool, &tool_args, "");
1204+
let after_baseline =
1205+
labels::ensure_integrity_baseline(&baseline_scope, integrity, &ctx);
1206+
1207+
assert_eq!(
1208+
after_baseline,
1209+
labels::project_github_label(&ctx),
1210+
"{} integrity should remain github-scoped after baseline enforcement",
1211+
tool
1212+
);
1213+
}
1214+
}
1215+
1216+
#[test]
1217+
fn repo_creation_integrity_preserved_after_baseline() {
1218+
let ctx = PolicyContext::default();
1219+
let tool_args = json!({ "name": "new-repo" });
1220+
for tool in &["create_repository", "fork_repository"] {
1221+
let (_, integrity, _) =
1222+
labels::apply_tool_labels(tool, &tool_args, "", vec![], vec![], String::new(), &ctx);
1223+
let baseline_scope = infer_scope_for_baseline(tool, &tool_args, "");
1224+
let after_baseline =
1225+
labels::ensure_integrity_baseline(&baseline_scope, integrity, &ctx);
1226+
1227+
assert_eq!(
1228+
after_baseline,
1229+
labels::writer_integrity("github", &ctx),
1230+
"{} integrity should remain github writer-scoped after baseline enforcement",
1231+
tool
1232+
);
1233+
}
1234+
}
1235+
11621236
#[test]
11631237
fn transfer_repository_integrity_is_blocked_after_ensure_baseline() {
11641238
// Verify that the is_blocked_tool + blocked_integrity override logic produces

test/integration/binary_test.go

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,47 +25,53 @@ func TestBinaryInvocation_RoutedMode(t *testing.T) {
2525
t.Skip("Skipping binary integration test in short mode")
2626
}
2727

28-
// Find the binary
2928
binaryPath := findBinary(t)
3029
t.Logf("Using binary: %s", binaryPath)
3130

32-
// Create a temporary config file
33-
configFile := createTempConfig(t, map[string]interface{}{
34-
"testserver": map[string]interface{}{
35-
"command": "docker",
36-
"args": []string{"run", "--rm", "-i", "alpine:latest", "echo"},
37-
},
38-
})
39-
defer os.Remove(configFile)
31+
// Use an in-process mock backend to avoid Docker dependency
32+
mockBackend := createMinimalMockMCPBackend(t)
33+
defer mockBackend.Close()
4034

41-
// Start the server process
4235
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
4336
defer cancel()
4437

45-
port := "13001" // Use a specific port for testing
38+
port := "13001"
4639
cmd := exec.CommandContext(ctx, binaryPath,
47-
"--config", configFile,
40+
"--config-stdin",
4841
"--listen", "127.0.0.1:"+port,
4942
"--routed",
5043
)
5144

52-
// Capture output for debugging
45+
configJSON := map[string]interface{}{
46+
"mcpServers": map[string]interface{}{
47+
"testserver": map[string]interface{}{
48+
"type": "http",
49+
"url": mockBackend.URL + "/mcp",
50+
},
51+
},
52+
"gateway": map[string]interface{}{
53+
"port": 13001,
54+
"domain": "localhost",
55+
"apiKey": "test-token",
56+
},
57+
}
58+
configBytes, _ := json.Marshal(configJSON)
59+
5360
var stdout, stderr bytes.Buffer
61+
cmd.Stdin = bytes.NewReader(configBytes)
5462
cmd.Stdout = &stdout
5563
cmd.Stderr = &stderr
5664

5765
if err := cmd.Start(); err != nil {
5866
t.Fatalf("Failed to start server: %v", err)
5967
}
6068

61-
// Ensure the process is killed at the end
6269
defer func() {
6370
if cmd.Process != nil {
6471
cmd.Process.Kill()
6572
}
6673
}()
6774

68-
// Wait for server to start
6975
serverURL := "http://127.0.0.1:" + port
7076
if !waitForServer(t, serverURL+"/health", 10*time.Second) {
7177
t.Logf("STDOUT: %s", stdout.String())
@@ -130,29 +136,43 @@ func TestBinaryInvocation_UnifiedMode(t *testing.T) {
130136
binaryPath := findBinary(t)
131137
t.Logf("Using binary: %s", binaryPath)
132138

133-
configFile := createTempConfig(t, map[string]interface{}{
134-
"backend1": map[string]interface{}{
135-
"command": "docker",
136-
"args": []string{"run", "--rm", "-i", "alpine:latest", "echo"},
137-
},
138-
"backend2": map[string]interface{}{
139-
"command": "docker",
140-
"args": []string{"run", "--rm", "-i", "alpine:latest", "echo"},
141-
},
142-
})
143-
defer os.Remove(configFile)
139+
// Use in-process mock backends to avoid Docker dependency
140+
mockBackend1 := createMinimalMockMCPBackend(t)
141+
defer mockBackend1.Close()
142+
mockBackend2 := createMinimalMockMCPBackend(t)
143+
defer mockBackend2.Close()
144144

145145
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
146146
defer cancel()
147147

148148
port := "13002"
149149
cmd := exec.CommandContext(ctx, binaryPath,
150-
"--config", configFile,
150+
"--config-stdin",
151151
"--listen", "127.0.0.1:"+port,
152152
"--unified",
153153
)
154154

155+
configJSON := map[string]interface{}{
156+
"mcpServers": map[string]interface{}{
157+
"backend1": map[string]interface{}{
158+
"type": "http",
159+
"url": mockBackend1.URL + "/mcp",
160+
},
161+
"backend2": map[string]interface{}{
162+
"type": "http",
163+
"url": mockBackend2.URL + "/mcp",
164+
},
165+
},
166+
"gateway": map[string]interface{}{
167+
"port": 13002,
168+
"domain": "localhost",
169+
"apiKey": "test-token",
170+
},
171+
}
172+
configBytes, _ := json.Marshal(configJSON)
173+
155174
var stdout, stderr bytes.Buffer
175+
cmd.Stdin = bytes.NewReader(configBytes)
156176
cmd.Stdout = &stdout
157177
cmd.Stderr = &stderr
158178

0 commit comments

Comments
 (0)