writeSecretFiles writes user secrets with file_path set to disk. Errors are logged but do not block workspace startup.
(ctx context.Context, logger slog.Logger, fs afero.Fs, homeDir string, secrets []agentsdk.WorkspaceSecret)
| 1632 | // writeSecretFiles writes user secrets with file_path set to disk. |
| 1633 | // Errors are logged but do not block workspace startup. |
| 1634 | func writeSecretFiles(ctx context.Context, logger slog.Logger, fs afero.Fs, homeDir string, secrets []agentsdk.WorkspaceSecret) { |
| 1635 | // Track resolved paths to detect collisions after ~/ expansion. |
| 1636 | // Two secrets with different file_path values can resolve to |
| 1637 | // the same absolute path (e.g. ~/x and /home/coder/x). The API |
| 1638 | // layer prevents duplicates on the raw file_path but cannot see |
| 1639 | // post-resolution collisions. We still write both, with the |
| 1640 | // later one winning, but log a warning so the conflict is |
| 1641 | // visible. |
| 1642 | seen := make(map[string]string, len(secrets)) |
| 1643 | |
| 1644 | for _, secret := range secrets { |
| 1645 | if secret.FilePath == "" { |
| 1646 | continue |
| 1647 | } |
| 1648 | |
| 1649 | filePath := secret.FilePath |
| 1650 | if strings.HasPrefix(filePath, "~/") { |
| 1651 | if homeDir == "" { |
| 1652 | logger.Warn(ctx, "skipping secret file with ~/ path: home directory unknown", |
| 1653 | slog.F("file_path", filePath), |
| 1654 | ) |
| 1655 | continue |
| 1656 | } |
| 1657 | filePath = filepath.Join(homeDir, filePath[2:]) |
| 1658 | } |
| 1659 | filePath = filepath.Clean(filePath) |
| 1660 | |
| 1661 | if original, ok := seen[filePath]; ok { |
| 1662 | // Known shortcoming: the winning secret is determined by the order |
| 1663 | // of secrets in the manifest, which is currently alphabetical by |
| 1664 | // secret name from ListUserSecretsWithValues. This ordering is not |
| 1665 | // user-controllable and has no semantic meaning; users should avoid |
| 1666 | // path collisions rather than rely on which secret wins. |
| 1667 | logger.Warn(ctx, "multiple secrets resolve to the same file path; later secret in manifest order will win (not user-controllable)", |
| 1668 | slog.F("resolved_path", filePath), |
| 1669 | slog.F("first_file_path", original), |
| 1670 | slog.F("conflicting_file_path", secret.FilePath), |
| 1671 | ) |
| 1672 | } |
| 1673 | seen[filePath] = secret.FilePath |
| 1674 | |
| 1675 | dir := filepath.Dir(filePath) |
| 1676 | if err := fs.MkdirAll(dir, 0o700); err != nil { |
| 1677 | logger.Warn(ctx, "failed to create directory for secret file", |
| 1678 | slog.F("file_path", filePath), |
| 1679 | slog.Error(err), |
| 1680 | ) |
| 1681 | continue |
| 1682 | } |
| 1683 | |
| 1684 | // The 0o600 perm only applies when the file is created. |
| 1685 | // If the file already exists, its permissions are |
| 1686 | // preserved. We only update the content. |
| 1687 | if err := afero.WriteFile(fs, filePath, secret.Value, 0o600); err != nil { |
| 1688 | logger.Warn(ctx, "failed to write secret file", |
| 1689 | slog.F("file_path", filePath), |
| 1690 | slog.Error(err), |
| 1691 | ) |