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
11 changes: 10 additions & 1 deletion internal/api/wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,16 @@ func (a *App) wrapInitCore(s *session.Session, sessionID string, req types.WrapI
// macOS intercepts through the system extension rather than a per-process
// wrapper, so it takes a different path entirely and returns here.
if runtime.GOOS == "darwin" {
return platformWrapInit(a, sessionID, req)
resp, status, err := platformWrapInit(a, sessionID, req)
if err != nil {
return resp, status, err
}
// The client applies these to the agent's environment, as on Linux.
// Before, macOS sent neither, so env_policy and env_inject did
// nothing under wrap.
resp.EnvInject = mergeEnvInject(a.cfg, a.policyEngineFor(s))
resp.EnvPolicy = a.wrapEnvPolicyWire(s, req)
return resp, status, nil
}

// Only supported on Linux (seccomp) otherwise
Expand Down
11 changes: 8 additions & 3 deletions internal/cli/wrap_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"os"
"syscall"

"github.com/diffsec/agentmon/internal/envinject"
"github.com/diffsec/agentmon/internal/wrapenv"
"github.com/diffsec/agentmon/pkg/types"
)

Expand All @@ -16,11 +18,15 @@ import (
// The wrap command registers the session with the server, which configures the ESF client
// to intercept execs from this session's process tree.
func platformSetupWrap(ctx context.Context, wrapResp types.WrapInitResponse, sessID string, agentPath string, agentArgs []string, cfg *clientConfig) (*wrapLaunchConfig, error) {
// env_policy filters the inherited environment, and env_inject is laid
// over the result, in the same order as on Linux.
env := envinject.Apply(
buildWrapEnv(wrapenv.Filter(os.Environ(), wrapResp.EnvPolicy), sessID, cfg.serverAddr, wrapResp.SafeToBypassShellShim),
wrapResp.EnvInject)

if wrapResp.WrapperBinary == "" {
// No wrapper needed on macOS — ES interception is handled by System Extension.
// The agent runs directly, and the ESF client intercepts its execs.
env := buildWrapEnv(os.Environ(), sessID, cfg.serverAddr, wrapResp.SafeToBypassShellShim)

return &wrapLaunchConfig{
command: agentPath,
args: agentArgs,
Expand All @@ -33,7 +39,6 @@ func platformSetupWrap(ctx context.Context, wrapResp types.WrapInitResponse, ses

// If the server returns a wrapper binary (e.g., agentmon-macwrap for sandboxing),
// use it as the launch command.
env := buildWrapEnv(os.Environ(), sessID, cfg.serverAddr, wrapResp.SafeToBypassShellShim)
for k, v := range wrapResp.WrapperEnv {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
Expand Down
35 changes: 35 additions & 0 deletions internal/cli/wrap_darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build darwin

package cli

import (
"context"
"slices"
"testing"

"github.com/diffsec/agentmon/pkg/types"
)

// Before, macOS wrap passed the inherited environment unchanged: the server
// sent no env_policy or env_inject, and the client applied neither.
func TestPlatformSetupWrapAppliesEnvPolicyAndInject(t *testing.T) {
t.Setenv("WRAPTEST_SECRET", "s3cret")
t.Setenv("WRAPTEST_KEEP", "yes")
resp := types.WrapInitResponse{
EnvPolicy: &types.EnvPolicyWire{Deny: []string{"WRAPTEST_SECRET"}},
EnvInject: map[string]string{"WRAPTEST_INJECTED": "1", "WRAPTEST_KEEP": "injected"},
}
lc, err := platformSetupWrap(context.Background(), resp, "sess-1", "/bin/echo", nil, &clientConfig{serverAddr: "http://127.0.0.1:1"})
if err != nil {
t.Fatal(err)
}
if slices.Contains(lc.env, "WRAPTEST_SECRET=s3cret") {
t.Error("env_policy deny did not remove WRAPTEST_SECRET")
}
if !slices.Contains(lc.env, "WRAPTEST_INJECTED=1") {
t.Error("env_inject did not add WRAPTEST_INJECTED")
}
if !slices.Contains(lc.env, "WRAPTEST_KEEP=injected") || slices.Contains(lc.env, "WRAPTEST_KEEP=yes") {
t.Error("env_inject did not replace WRAPTEST_KEEP")
}
}
71 changes: 71 additions & 0 deletions scripts/test-macos-wrap-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Hardware test: `agentmon wrap` applies sandbox.env_inject and env_policy to
# the agent's environment on macOS, as it does on Linux.
#
# Before build 1684, the macOS wrap-init reply carried neither, so the agent
# got the caller's environment unchanged.
#
# The daemon configuration needs an env_inject entry. The shipped
# configuration injects BASH_ENV. The env_policy check runs only when
# sandbox.wrap_env_policy.enabled is true. It then expects agent-default to
# remove AWS_SECRET_ACCESS_KEY.
#
# Run it on a Mac where AgentMon.app is installed, its extension is enabled,
# and the daemon is running. Run it before each release.
set -euo pipefail

A=${AGENTMON:-/Applications/AgentMon.app/Contents/MacOS/agentmon}
CONFIG=${AGENTMON_CONFIG:-"$HOME/Library/Application Support/agentmon/config.yml"}
PY=${PYTHON:-}
if [ -z "$PY" ]; then
for p in /opt/homebrew/bin/python3 /usr/local/bin/python3; do
if [ -x "$p" ]; then PY=$p; break; fi
done
fi
[ -n "$PY" ] || { echo "no python3 found; set PYTHON" >&2; exit 2; }

WS=$(mktemp -d "${TMPDIR:-/tmp}/amenv.XXXXXX")
WS=$(cd "$WS" && pwd -P)
status=0
trap 'rm -rf "$WS"' EXIT

ok() { echo "ok $*"; }
fail() { echo "FAIL $*"; status=1; }

# The first env_inject key in the daemon configuration, and its value.
read -r KEY VAL < <(awk '
/^[^ #]/ { insb = ($0 ~ /^sandbox:/) }
insb && /^ env_inject:/ { ine = 1; next }
ine && /^ [A-Za-z_][A-Za-z0-9_]*:/ {
k = $1; sub(":", "", k); v = $2; gsub("\"", "", v); print k, v; exit }
ine && /^ [^ ]/ { ine = 0 }
' "$CONFIG") || true
[ -n "${KEY:-}" ] || { echo "no sandbox.env_inject entry in $CONFIG" >&2; exit 2; }

got=$(cd "$WS" && env "$KEY=caller-value" AWS_SECRET_ACCESS_KEY=caller-secret \
"$A" wrap --policy agent-default --report=false -- \
"$PY" -c "import os; print(os.environ.get('$KEY', '-'), os.environ.get('AWS_SECRET_ACCESS_KEY', '-'))" \
2>>"$WS/err" | tail -n 1) || true
read -r injected secret <<<"$got"

if [ "${injected:-}" = "$VAL" ]; then
ok "env_inject sets $KEY=$VAL under wrap"
else
fail "$KEY under wrap: got '${injected:-}', want '$VAL'"
fi

if awk '/^ wrap_env_policy:/ { w = 1; next } w && /enabled: *true/ { f = 1 } w && /^ [^ ]/ { w = 0 } END { exit !f }' "$CONFIG"; then
if [ "${secret:-}" = "-" ]; then
ok "env_policy removes AWS_SECRET_ACCESS_KEY under wrap"
else
fail "AWS_SECRET_ACCESS_KEY under wrap: got '${secret:-}', want it removed"
fi
else
echo "skip env_policy: sandbox.wrap_env_policy.enabled is not true"
fi

if [ $status -ne 0 ] && [ -s "$WS/err" ]; then
echo "wrap stderr:"
sed 's/^/ /' "$WS/err"
fi
exit $status
Loading