Context
Scion's file-secrets pipeline already handles host-file → container-path projection end-to-end:
writeFileSecrets at pkg/runtime/common.go:607-682 stages files on the host, creates parent directories, and emits host:container:ro mount specs
- Plumbed through all three runtimes:
pkg/runtime/docker.go:53, pkg/runtime/podman.go:131, pkg/runtime/apple_container.go:53
scion hub secret set --type file --target <container-path> <KEY> @<host-path> at cmd/hub_secret.go:105 exercises this path today
The only limitation: api.RequiredSecret (pkg/api/types.go:472-482) declares {Key, Type, Target} but not a local-host origin. Resolution of a Key into a ResolvedSecret goes through a Hub.
Users running Scion locally (no Hub) have no way to ask "inject ~/.ssh/id_ed25519 at /home/scion/.ssh/id_ed25519" through a config-driven path. They fall back to either:
- Raw
volumes: mounts (works but sits at a different abstraction level than other secrets)
- Hand-placing files in template
home/ directories (leaks into the grove template tree and potentially into git)
- Standing up a Hub (overkill for single-user local development)
This matters for any Scion consumer whose agents need SSH keys, gh CLI tokens, kubeconfig, cloud credentials, or database URLs to do real work.
Adjacent history: #94 (closed) fixed a decode bug on the same writeFileSecrets path. This ask adds a local-origin resolution step before the Hub step, reusing the same downstream pipeline.
Proposal
Add a HostPath field to a local SecretMount type (or extend RequiredSecret) and resolve it to a ResolvedSecret on the host side before the existing Hub-resolution step runs. All downstream machinery — writeFileSecrets, runtime mount injection — reused unchanged.
Shape in settings.yaml:
harness_configs:
claude:
secrets:
- key: ssh_key
type: file
host_path: ~/.ssh/id_ed25519
target: /home/scion/.ssh/id_ed25519
- key: gh_hosts
type: file
host_path: ~/.config/gh/hosts.yml
target: /home/scion/.config/gh/hosts.yml
- key: kubeconfig
type: file
host_path: ~/.kube/config
target: /home/scion/.kube/config
Implementation sketch:
- Extend the struct in
pkg/config/settings_v1.go with an optional HostPath string on the secrets-declaration type (either RequiredSecret directly or a new local-only SecretMount sibling to keep the Hub-facing surface clean).
- In
pkg/agent/run.go around line 815 (before the m.Runtime.Run call), walk the config's declared secrets and for each one with HostPath set:
- Tilde-expand the path; resolve against the user's home
- Read the file; base64-encode
- Construct
api.ResolvedSecret{Type: file, Target, Value: base64(contents)}
- Prepend/append to
runCfg.ResolvedSecrets
- If both
HostPath and Hub resolution apply to the same key, precedence is explicit-loud failure (config error) rather than silent last-wins.
- Read-only by default (already true in
writeFileSecrets).
Behavior for missing host file: fail loudly at agent start with a clear error message (secret "ssh_key": host_path ~/.ssh/id_ed25519 not found), not silent skip. Consistent with how auth propagation fails today when a required credential file is absent.
Why this shape
- Additive, not disruptive. No existing
settings.yaml files break; host_path is optional.
- Reuses end-to-end plumbing.
writeFileSecrets, mount-spec generation, runtime layer — all untouched.
- Single mechanism for all file secrets. Covers ssh, gh, kubeconfig, aws, stripe, db URLs, the lot. No ssh-specific flags, no per-credential-type shims.
- No Hub required. Local-dev ergonomics improved without weakening Hub-based deployments.
- Precedent in the codebase.
OverlayFileSecrets at pkg/harness/auth.go:113-136 already does host-file-to-ResolvedSecret conversion for auth-specific paths; this generalizes that pattern.
Acceptance
HostPath field exists on the secrets-declaration type in settings_v1.go, documented in the schema comment
- Local host files resolve to
ResolvedSecret entries before runtime Run, verified by a unit test in pkg/agent/run_test.go (or equivalent) that exercises the resolution with a tempdir-based host file
- Integration test confirms
scion start with a host_path-declared secret lands the file at the target path inside a live container, readable by the container user
- Clear error on missing host file; exit code non-zero, message identifies the offending key
- Existing Hub-based secret resolution unaffected — test matrix includes both paths
settings.yaml schema doc + at least one example showing the SSH-key use case
Out of scope
- Hot-reload of secrets into running agents (orthogonal — belongs with a pre-start-hook primitive)
- Templated container targets (e.g.,
target: /home/{{ user }}/.ssh/id_rsa) — ship with literal paths first
- Write-mode secrets (stays read-only like
writeFileSecrets already enforces)
- Replacement for Hub-based flows — both coexist
Open design questions
- New
SecretMount type vs. HostPath field on existing RequiredSecret? Leaning toward the former — keeps the Hub-required declaration surface clean and makes the local-origin intent explicit in the config.
- Glob support (
host_path: ~/.ssh/id_*)? Probably not for v1 — explicit > implicit, and expansion semantics would need their own spec.
- Environment-variable expansion in
host_path? Tilde-only is minimal and safe for v1.
Context
Scion's file-secrets pipeline already handles host-file → container-path projection end-to-end:
writeFileSecretsatpkg/runtime/common.go:607-682stages files on the host, creates parent directories, and emitshost:container:romount specspkg/runtime/docker.go:53,pkg/runtime/podman.go:131,pkg/runtime/apple_container.go:53scion hub secret set --type file --target <container-path> <KEY> @<host-path>atcmd/hub_secret.go:105exercises this path todayThe only limitation:
api.RequiredSecret(pkg/api/types.go:472-482) declares{Key, Type, Target}but not a local-host origin. Resolution of aKeyinto aResolvedSecretgoes through a Hub.Users running Scion locally (no Hub) have no way to ask "inject
~/.ssh/id_ed25519at/home/scion/.ssh/id_ed25519" through a config-driven path. They fall back to either:volumes:mounts (works but sits at a different abstraction level than other secrets)home/directories (leaks into the grove template tree and potentially into git)This matters for any Scion consumer whose agents need SSH keys, gh CLI tokens, kubeconfig, cloud credentials, or database URLs to do real work.
Adjacent history: #94 (closed) fixed a decode bug on the same
writeFileSecretspath. This ask adds a local-origin resolution step before the Hub step, reusing the same downstream pipeline.Proposal
Add a
HostPathfield to a localSecretMounttype (or extendRequiredSecret) and resolve it to aResolvedSecreton the host side before the existing Hub-resolution step runs. All downstream machinery —writeFileSecrets, runtime mount injection — reused unchanged.Shape in
settings.yaml:Implementation sketch:
pkg/config/settings_v1.gowith an optionalHostPath stringon the secrets-declaration type (eitherRequiredSecretdirectly or a new local-onlySecretMountsibling to keep the Hub-facing surface clean).pkg/agent/run.goaround line 815 (before them.Runtime.Runcall), walk the config's declared secrets and for each one withHostPathset:api.ResolvedSecret{Type: file, Target, Value: base64(contents)}runCfg.ResolvedSecretsHostPathand Hub resolution apply to the same key, precedence is explicit-loud failure (config error) rather than silent last-wins.writeFileSecrets).Behavior for missing host file: fail loudly at agent start with a clear error message (
secret "ssh_key": host_path ~/.ssh/id_ed25519 not found), not silent skip. Consistent with how auth propagation fails today when a required credential file is absent.Why this shape
settings.yamlfiles break;host_pathis optional.writeFileSecrets, mount-spec generation, runtime layer — all untouched.OverlayFileSecretsatpkg/harness/auth.go:113-136already does host-file-to-ResolvedSecret conversion for auth-specific paths; this generalizes that pattern.Acceptance
HostPathfield exists on the secrets-declaration type insettings_v1.go, documented in the schema commentResolvedSecretentries before runtime Run, verified by a unit test inpkg/agent/run_test.go(or equivalent) that exercises the resolution with a tempdir-based host filescion startwith ahost_path-declared secret lands the file at the target path inside a live container, readable by the container usersettings.yamlschema doc + at least one example showing the SSH-key use caseOut of scope
target: /home/{{ user }}/.ssh/id_rsa) — ship with literal paths firstwriteFileSecretsalready enforces)Open design questions
SecretMounttype vs.HostPathfield on existingRequiredSecret? Leaning toward the former — keeps the Hub-required declaration surface clean and makes the local-origin intent explicit in the config.host_path: ~/.ssh/id_*)? Probably not for v1 — explicit > implicit, and expansion semantics would need their own spec.host_path? Tilde-only is minimal and safe for v1.