mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Fix local subscription CLI service setup
This commit is contained in:
+17
@@ -420,6 +420,23 @@ does not forward API-key environment variables such as `OPENAI_API_KEY` or
|
||||
`ANTHROPIC_API_KEY`, Pulse secrets, cloud credentials, or unrelated tokens,
|
||||
preventing an installed API key from silently changing the billing route.
|
||||
|
||||
On a standard Linux systemd install, Pulse runs as the `pulse` account with
|
||||
`HOME=/opt/pulse`. Install the selected CLI for that account (its supported
|
||||
user-local binary directory is `/opt/pulse/.local/bin`) and complete the login
|
||||
as that account; a CLI installed or authenticated only as `root` is
|
||||
intentionally unavailable to Pulse. For Claude, verify the service identity
|
||||
with:
|
||||
|
||||
```bash
|
||||
sudo -u pulse env HOME=/opt/pulse PATH=/opt/pulse/.local/bin:/usr/local/bin:/usr/bin:/bin claude auth status --json
|
||||
```
|
||||
|
||||
If it is not logged in, run `claude auth login` with the same `sudo -u pulse`
|
||||
environment, restart `pulse.service`, and retry the provider test. Pulse also
|
||||
accepts an absolute executable override through `PULSE_CLAUDE_CLI_PATH` or
|
||||
`PULSE_CODEX_CLI_PATH` in `/etc/pulse/.env`; the target must remain executable
|
||||
by the `pulse` account and its credentials must still belong to that account.
|
||||
|
||||
The child CLI is not given infrastructure authority. Each invocation runs in a
|
||||
new temporary directory, with user extensions disabled, no Pulse MCP server,
|
||||
no approval capability, and a structured output schema. It returns one proposed
|
||||
|
||||
@@ -431,6 +431,14 @@ refresh, export, or forward that login, and its child environment is an
|
||||
allowlist that excludes API keys, Pulse secrets, cloud credentials, and
|
||||
unrelated tokens. It must never fall back to an API-key provider.
|
||||
|
||||
CLI discovery is scoped to the Pulse service identity: the service `PATH`,
|
||||
that identity's `.local/bin`, `bin`, and `.npm-global/bin`, or an explicit
|
||||
absolute `PULSE_CLAUDE_CLI_PATH` / `PULSE_CODEX_CLI_PATH` override. It must not
|
||||
search or expose another user's home. Missing executables and missing
|
||||
first-party login are typed local-setup failures, not provider reachability
|
||||
failures, and provider-test plus Patrol diagnostics must tell the operator to
|
||||
install and authenticate the CLI as the account running Pulse (#1742).
|
||||
|
||||
Each subscription-agent call is a single structured provider turn in a fresh
|
||||
temporary working directory. Codex runs ephemeral with user configuration
|
||||
ignored and a read-only sandbox. Claude runs without session persistence or
|
||||
|
||||
@@ -3823,6 +3823,11 @@ the authoritative analysis outcome.
|
||||
listing must accept that keyless custom route without emitting an empty
|
||||
Authorization header. Custom model-list results retain every non-empty
|
||||
opaque ID and carry server-authored `provider: "openai"` identity.
|
||||
Local subscription test and Patrol failures must distinguish missing CLI
|
||||
installation or login for the Pulse service identity from network
|
||||
reachability: the response keeps the existing bounded diagnostic envelope,
|
||||
classifies the route as not configured, and returns service-account setup
|
||||
remediation without raw CLI output (#1742).
|
||||
`remove_providers` is the complete provider lifecycle mutation: it removes
|
||||
provider-owned secrets, endpoints and runtime options, clears selected
|
||||
models for that provider, invalidates model inventory, and disables Pulse
|
||||
|
||||
@@ -2778,6 +2778,14 @@ not exist at all: the installer always writes the unit file itself, even
|
||||
where systemctl cannot run, so a missing unit is a broken installation
|
||||
rather than an unprivileged-container quirk and must fail the run loudly.
|
||||
|
||||
The generated `pulse.service` unit also owns the local subscription-agent
|
||||
execution identity. It pins `HOME` to the Pulse install directory and prepends
|
||||
that identity's `.local/bin` plus the install `bin` directory to `PATH`, while
|
||||
retaining `User=pulse` and `ProtectHome=true`. This makes a Claude or Codex CLI
|
||||
installed and authenticated for the Pulse account discoverable without
|
||||
exposing a root or interactive user's home and lets updates migrate the unit
|
||||
contract onto existing systemd installations (#1742).
|
||||
|
||||
Changes to the generated units must be able to reach already-deployed boxes.
|
||||
A box installed before the sandbox was widened runs the installer from a
|
||||
`pulse-update.service` whose `ReadWritePaths` excludes the helper and unit
|
||||
|
||||
@@ -420,6 +420,23 @@ does not forward API-key environment variables such as `OPENAI_API_KEY` or
|
||||
`ANTHROPIC_API_KEY`, Pulse secrets, cloud credentials, or unrelated tokens,
|
||||
preventing an installed API key from silently changing the billing route.
|
||||
|
||||
On a standard Linux systemd install, Pulse runs as the `pulse` account with
|
||||
`HOME=/opt/pulse`. Install the selected CLI for that account (its supported
|
||||
user-local binary directory is `/opt/pulse/.local/bin`) and complete the login
|
||||
as that account; a CLI installed or authenticated only as `root` is
|
||||
intentionally unavailable to Pulse. For Claude, verify the service identity
|
||||
with:
|
||||
|
||||
```bash
|
||||
sudo -u pulse env HOME=/opt/pulse PATH=/opt/pulse/.local/bin:/usr/local/bin:/usr/bin:/bin claude auth status --json
|
||||
```
|
||||
|
||||
If it is not logged in, run `claude auth login` with the same `sudo -u pulse`
|
||||
environment, restart `pulse.service`, and retry the provider test. Pulse also
|
||||
accepts an absolute executable override through `PULSE_CLAUDE_CLI_PATH` or
|
||||
`PULSE_CODEX_CLI_PATH` in `/etc/pulse/.env`; the target must remain executable
|
||||
by the `pulse` account and its credentials must still belong to that account.
|
||||
|
||||
The child CLI is not given infrastructure authority. Each invocation runs in a
|
||||
new temporary directory, with user extensions disabled, no Pulse MCP server,
|
||||
no approval capability, and a structured output schema. It returns one proposed
|
||||
|
||||
+2
-1
@@ -4513,7 +4513,8 @@ Restart=always
|
||||
RestartSec=3
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
Environment="HOME=$INSTALL_DIR"
|
||||
Environment="PATH=$INSTALL_DIR/.local/bin:$INSTALL_DIR/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
Environment="PULSE_DATA_DIR=$CONFIG_DIR"
|
||||
Environment="PULSE_DEPLOYMENT_METHOD=systemd"
|
||||
EnvironmentFile=-$CONFIG_DIR/.env
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -153,6 +154,15 @@ func ClassifyPatrolRuntimeFailure(err error) PatrolRuntimeFailureDiagnostic {
|
||||
}
|
||||
|
||||
func ClassifyProviderConnectionFailure(err error) PatrolRuntimeFailureDiagnostic {
|
||||
if setup, ok := subscriptionAgentSetupFailure(err); ok {
|
||||
return PatrolRuntimeFailureDiagnostic{
|
||||
Title: "Local " + setup.displayName + " CLI not ready",
|
||||
Summary: "Local " + setup.displayName + " CLI not ready",
|
||||
Cause: PatrolFailureCauseProviderNotConfigured,
|
||||
Description: "Pulse cannot use the local " + setup.displayName + " subscription because its CLI executable or login is unavailable to the operating-system account running Pulse.",
|
||||
Recommendation: setup.recommendation,
|
||||
}
|
||||
}
|
||||
failure := patrolRuntimeFailureFromError(err)
|
||||
diagnostic := PatrolRuntimeFailureDiagnostic{
|
||||
Title: "Provider connection issue",
|
||||
@@ -223,6 +233,35 @@ func ClassifyProviderConnectionFailure(err error) PatrolRuntimeFailureDiagnostic
|
||||
return diagnostic
|
||||
}
|
||||
|
||||
type subscriptionAgentSetupCopy struct {
|
||||
displayName string
|
||||
recommendation string
|
||||
}
|
||||
|
||||
func subscriptionAgentSetupFailure(err error) (subscriptionAgentSetupCopy, bool) {
|
||||
var setupErr *providers.SubscriptionAgentSetupError
|
||||
if !errors.As(err, &setupErr) {
|
||||
return subscriptionAgentSetupCopy{}, false
|
||||
}
|
||||
switch setupErr.Agent {
|
||||
case providers.SubscriptionAgentClaude:
|
||||
return subscriptionAgentSetupCopy{
|
||||
displayName: "Claude",
|
||||
recommendation: "Install the Claude CLI and run `claude auth login` as the same account that runs Pulse. On standard systemd installs, use the `pulse` account with home `/opt/pulse`; restart Pulse, then retry.",
|
||||
}, true
|
||||
case providers.SubscriptionAgentCodex:
|
||||
return subscriptionAgentSetupCopy{
|
||||
displayName: "Codex",
|
||||
recommendation: "Install the Codex CLI and run `codex login` as the same account that runs Pulse. On standard systemd installs, use the `pulse` account with home `/opt/pulse`; restart Pulse, then retry.",
|
||||
}, true
|
||||
default:
|
||||
return subscriptionAgentSetupCopy{
|
||||
displayName: "subscription",
|
||||
recommendation: "Install and sign in to the local subscription CLI as the same operating-system account that runs Pulse, restart Pulse, then retry.",
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
// patrolRuntimeFailureFromError classifies an error with no knowledge of the
|
||||
// run context. Cancellation is then recognised only when the error actually
|
||||
// wraps context.Canceled. Callers that hold the run's context should use
|
||||
@@ -251,6 +290,7 @@ func patrolRuntimeFailureFromErrorCtx(ctx context.Context, err error) patrolRunt
|
||||
Detail: detail,
|
||||
}
|
||||
|
||||
setup, setupFailure := subscriptionAgentSetupFailure(err)
|
||||
switch {
|
||||
case cancelled:
|
||||
failure.Title = "Pulse Patrol: Analysis interrupted"
|
||||
@@ -258,6 +298,12 @@ func patrolRuntimeFailureFromErrorCtx(ctx context.Context, err error) patrolRunt
|
||||
failure.Cause = PatrolFailureCauseInterrupted
|
||||
failure.Description = "The Patrol run was cancelled before the provider finished, either by an operator cancel or because the client connection closed mid-analysis. An interrupted run is not evidence about the provider or model."
|
||||
failure.Recommendation = "Run the analysis again when you are ready. If you did not cancel it, check for reverse proxies or load balancers that close long-running requests."
|
||||
case setupFailure:
|
||||
failure.Title = "Pulse Patrol: Local " + setup.displayName + " CLI not ready"
|
||||
failure.Summary = "Local " + setup.displayName + " CLI not ready"
|
||||
failure.Cause = PatrolFailureCauseProviderNotConfigured
|
||||
failure.Description = "Pulse Patrol cannot use the local " + setup.displayName + " subscription because its CLI executable or login is unavailable to the operating-system account running Pulse."
|
||||
failure.Recommendation = setup.recommendation
|
||||
case patrolMalformedToolHistory(lower):
|
||||
failure.Title = "Pulse Patrol: Malformed tool-call conversation history"
|
||||
failure.Summary = "Malformed tool-call conversation history"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/tools"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
@@ -213,6 +214,28 @@ func TestClassifyProviderConnectionFailureUsesNeutralProviderCopy(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionAgentSetupFailureUsesLocalServiceAccountRemediation(t *testing.T) {
|
||||
err := &providers.SubscriptionAgentSetupError{
|
||||
Agent: providers.SubscriptionAgentClaude,
|
||||
Issue: providers.SubscriptionAgentExecutableMissing,
|
||||
}
|
||||
diagnostic := ClassifyProviderConnectionFailure(err)
|
||||
if diagnostic.Summary != "Local Claude CLI not ready" || diagnostic.Cause != PatrolFailureCauseProviderNotConfigured {
|
||||
t.Fatalf("connection diagnostic = %#v", diagnostic)
|
||||
}
|
||||
if !strings.Contains(diagnostic.Recommendation, "`pulse` account") || !strings.Contains(diagnostic.Recommendation, "`claude auth login`") {
|
||||
t.Fatalf("connection recommendation lacks service-account remediation: %q", diagnostic.Recommendation)
|
||||
}
|
||||
|
||||
failure := patrolRuntimeFailureFromError(err)
|
||||
if failure.Summary != "Local Claude CLI not ready" || failure.Cause != PatrolFailureCauseProviderNotConfigured {
|
||||
t.Fatalf("Patrol failure = %#v", failure)
|
||||
}
|
||||
if strings.Contains(failure.Description, "/root") || strings.Contains(failure.Recommendation, "/root") {
|
||||
t.Fatalf("subscription setup copy encouraged sharing a privileged home: %#v", failure)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyProviderConnectionFailureKeepsSafeModelUnavailableCopy(t *testing.T) {
|
||||
diagnostic := ClassifyProviderConnectionFailure(errors.New(`connected to Ollama but model "qwen3.5:2b" is not available; found: qwen3.5:4b`))
|
||||
|
||||
|
||||
@@ -22,6 +22,29 @@ import (
|
||||
// continues to own and execute every infrastructure tool call.
|
||||
type SubscriptionAgent string
|
||||
|
||||
type SubscriptionAgentSetupIssue string
|
||||
|
||||
const (
|
||||
SubscriptionAgentExecutableMissing SubscriptionAgentSetupIssue = "executable_missing"
|
||||
SubscriptionAgentLoginMissing SubscriptionAgentSetupIssue = "login_missing"
|
||||
)
|
||||
|
||||
// SubscriptionAgentSetupError distinguishes local CLI installation/login
|
||||
// faults from network provider failures. Callers can give operators the
|
||||
// service-account remediation without exposing raw CLI output.
|
||||
type SubscriptionAgentSetupError struct {
|
||||
Agent SubscriptionAgent
|
||||
Issue SubscriptionAgentSetupIssue
|
||||
}
|
||||
|
||||
func (e *SubscriptionAgentSetupError) Error() string {
|
||||
name := subscriptionAgentCommandName(e.Agent)
|
||||
if e.Issue == SubscriptionAgentLoginMissing {
|
||||
return fmt.Sprintf("%s CLI is not signed in for the account running Pulse", name)
|
||||
}
|
||||
return fmt.Sprintf("%s CLI is not installed for the account running Pulse or is not on its service PATH", name)
|
||||
}
|
||||
|
||||
const (
|
||||
SubscriptionAgentCodex SubscriptionAgent = "codex-subscription"
|
||||
SubscriptionAgentClaude SubscriptionAgent = "claude-subscription"
|
||||
@@ -197,7 +220,7 @@ func (c *SubscriptionAgentClient) TestConnection(ctx context.Context) error {
|
||||
}
|
||||
if c.agent == SubscriptionAgentCodex {
|
||||
if !strings.Contains(strings.ToLower(string(out)), "logged in using chatgpt") {
|
||||
return errors.New("Codex CLI is not signed in with ChatGPT")
|
||||
return &SubscriptionAgentSetupError{Agent: SubscriptionAgentCodex, Issue: SubscriptionAgentLoginMissing}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -209,7 +232,7 @@ func (c *SubscriptionAgentClient) TestConnection(ctx context.Context) error {
|
||||
return fmt.Errorf("decode Claude authentication status: %w", err)
|
||||
}
|
||||
if !status.LoggedIn || status.AuthMethod != "claude.ai" {
|
||||
return errors.New("Claude CLI is not signed in with a Claude plan")
|
||||
return &SubscriptionAgentSetupError{Agent: SubscriptionAgentClaude, Issue: SubscriptionAgentLoginMissing}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -418,9 +441,9 @@ func (c *SubscriptionAgentClient) run(ctx context.Context, name string, args []s
|
||||
ctx, cancel = context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
}
|
||||
path, err := exec.LookPath(name)
|
||||
path, err := resolveSubscriptionAgentCommand(c.agent, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s CLI is not installed or not on PATH", name)
|
||||
return nil, err
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, path, args...)
|
||||
cmd.Env = subscriptionAgentEnvironment(os.Environ())
|
||||
@@ -460,6 +483,64 @@ func (c *SubscriptionAgentClient) run(ctx context.Context, name string, args []s
|
||||
return stdout.buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func subscriptionAgentCommandName(agent SubscriptionAgent) string {
|
||||
switch agent {
|
||||
case SubscriptionAgentCodex:
|
||||
return "codex"
|
||||
case SubscriptionAgentClaude:
|
||||
return "claude"
|
||||
default:
|
||||
return string(agent)
|
||||
}
|
||||
}
|
||||
|
||||
func subscriptionAgentCommandOverride(agent SubscriptionAgent) string {
|
||||
switch agent {
|
||||
case SubscriptionAgentCodex:
|
||||
return strings.TrimSpace(os.Getenv("PULSE_CODEX_CLI_PATH"))
|
||||
case SubscriptionAgentClaude:
|
||||
return strings.TrimSpace(os.Getenv("PULSE_CLAUDE_CLI_PATH"))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func resolveSubscriptionAgentCommand(agent SubscriptionAgent, name string) (string, error) {
|
||||
if override := subscriptionAgentCommandOverride(agent); override != "" {
|
||||
if path, ok := executableSubscriptionAgentPath(override); ok {
|
||||
return path, nil
|
||||
}
|
||||
return "", &SubscriptionAgentSetupError{Agent: agent, Issue: SubscriptionAgentExecutableMissing}
|
||||
}
|
||||
if path, err := exec.LookPath(name); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
for _, relative := range []string{
|
||||
filepath.Join(".local", "bin", name),
|
||||
filepath.Join("bin", name),
|
||||
filepath.Join(".npm-global", "bin", name),
|
||||
} {
|
||||
if path, ok := executableSubscriptionAgentPath(filepath.Join(home, relative)); ok {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return "", &SubscriptionAgentSetupError{Agent: agent, Issue: SubscriptionAgentExecutableMissing}
|
||||
}
|
||||
|
||||
func executableSubscriptionAgentPath(path string) (string, bool) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" || !filepath.IsAbs(path) {
|
||||
return "", false
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0111 == 0 {
|
||||
return "", false
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
func subscriptionAgentEnvironment(environment []string) []string {
|
||||
allowed := map[string]bool{"HOME": true, "USER": true, "LOGNAME": true, "PATH": true, "TMPDIR": true, "SHELL": true, "LANG": true, "LC_ALL": true, "TERM": true, "CODEX_HOME": true, "CLAUDE_CONFIG_DIR": true, "XDG_CONFIG_HOME": true, "XDG_CACHE_HOME": true, "SSL_CERT_FILE": true, "SSL_CERT_DIR": true, "NO_PROXY": true, "no_proxy": true}
|
||||
out := make([]string, 0, len(allowed))
|
||||
|
||||
@@ -32,6 +32,52 @@ func TestSubscriptionAgentEnvironmentDoesNotForwardSecrets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSubscriptionAgentCommandFindsServiceHomeLocalBin(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("service-home executable discovery uses POSIX executable bits")
|
||||
}
|
||||
home := t.TempDir()
|
||||
binDir := filepath.Join(home, ".local", "bin")
|
||||
if err := os.MkdirAll(binDir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
claudePath := filepath.Join(binDir, "claude")
|
||||
writeExecutable(t, claudePath, "#!/bin/sh\nexit 0\n")
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
|
||||
got, err := resolveSubscriptionAgentCommand(SubscriptionAgentClaude, "claude")
|
||||
if err != nil || got != claudePath {
|
||||
t.Fatalf("service-home Claude command = %q, %v; want %q", got, err, claudePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionAgentConnectionReportsServiceAccountSetupFailures(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fake subscription CLI uses a POSIX shell script")
|
||||
}
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
client := NewSubscriptionAgentClient(SubscriptionAgentClaude, "sonnet", subscriptionAgentTestDeadline(time.Second))
|
||||
|
||||
err := client.TestConnection(context.Background())
|
||||
var setupErr *SubscriptionAgentSetupError
|
||||
if !errors.As(err, &setupErr) || setupErr.Agent != SubscriptionAgentClaude || setupErr.Issue != SubscriptionAgentExecutableMissing {
|
||||
t.Fatalf("missing executable error = %#v, want Claude executable setup error", err)
|
||||
}
|
||||
|
||||
binDir := filepath.Join(home, ".local", "bin")
|
||||
if err := os.MkdirAll(binDir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeExecutable(t, filepath.Join(binDir, "claude"), "#!/bin/sh\nprintf '%s' '{\"loggedIn\":false,\"authMethod\":\"none\"}'\n")
|
||||
err = client.TestConnection(context.Background())
|
||||
if !errors.As(err, &setupErr) || setupErr.Agent != SubscriptionAgentClaude || setupErr.Issue != SubscriptionAgentLoginMissing {
|
||||
t.Fatalf("missing login error = %#v, want Claude login setup error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCappedBufferBoundsChildOutput(t *testing.T) {
|
||||
buffer := cappedBuffer{maxBytes: 4}
|
||||
if n, err := buffer.Write([]byte("abcdef")); err != nil || n != 6 {
|
||||
|
||||
@@ -89,6 +89,23 @@ func TestRootInstallScriptArchiveSupportContract(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootInstallSystemdServiceExposesOnlyPulseOwnedSubscriptionCLIHome(t *testing.T) {
|
||||
body := extractRootInstallShellFunction(t, "install_systemd_service")
|
||||
for _, required := range []string{
|
||||
`Environment="HOME=$INSTALL_DIR"`,
|
||||
`Environment="PATH=$INSTALL_DIR/.local/bin:$INSTALL_DIR/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"`,
|
||||
`User=pulse`,
|
||||
`ProtectHome=true`,
|
||||
} {
|
||||
if !strings.Contains(body, required) {
|
||||
t.Fatalf("systemd service missing subscription CLI boundary %q", required)
|
||||
}
|
||||
}
|
||||
if strings.Contains(body, "/root/") {
|
||||
t.Fatal("systemd service must not expose a privileged home to local subscription CLIs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootInstallScriptStagesUpdateBeforeStoppingService(t *testing.T) {
|
||||
downloadPulse := extractRootInstallShellFunction(t, "download_pulse")
|
||||
orderedSteps := []string{
|
||||
|
||||
@@ -181,6 +181,11 @@ class AIRuntimeDocsPolicyTest(unittest.TestCase):
|
||||
self.assertIn("inference_route=coding_plan_allowance", content)
|
||||
self.assertIn("per-run monetary cost unknown", normalized_content)
|
||||
self.assertIn("standard Z.ai `/api/paas/` endpoint remains a `metered_api` route", normalized_content)
|
||||
self.assertIn("Pulse runs as the `pulse` account with `HOME=/opt/pulse`", normalized_content)
|
||||
self.assertIn("a CLI installed or authenticated only as `root` is intentionally unavailable", normalized_content)
|
||||
self.assertIn("claude auth status --json", content)
|
||||
self.assertIn("PULSE_CLAUDE_CLI_PATH", content)
|
||||
self.assertIn("PULSE_CODEX_CLI_PATH", content)
|
||||
|
||||
def test_public_ai_privacy_copy_discloses_outbound_usage_telemetry(self) -> None:
|
||||
content = read_repo_text("docs/AI.md")
|
||||
|
||||
Reference in New Issue
Block a user