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
16 changes: 15 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ elevated launches always read the machine config dir).
| Health (as launch account) | `va doctor` (syntax / config; offline by design) |
| Pre-flight: refs resolve in vault | `va secrets validate` (live; needs manager token; covers every harness manifest **and** every `extra_manifest`) |
| Pre-flight: shape only | `va secrets validate --offline` |
| Launch harness | `va claude` / `va codex` / `va grok` / `va kimi` / `va agy` / `va bash` |
| Launch harness | `va claude` / `va codex` / `va grok` / `va kimi` / `va agy` / `va muse` / `va bash` |
| **This launch only: other manifest** | `va -m readonly.env.tpl claude` |
| Interactive pick + optional -m | `va -m narrow.env.tpl pick` |
| One-shot command | `va run -m REFS --backend bitwarden -- cmd…` |
Expand Down Expand Up @@ -117,6 +117,20 @@ authentication. AGY reads an injected `GEMINI_API_KEY` only when its settings
select `modelProvider = gemini`. Authentication details:
https://antigravity.google/docs/cli/install/

### Muse Code

The shipped / auto Harness runs bare `muse` with `workdir = caller`. Muse owns
its permission settings. Arguments pass through unchanged: `va muse --yolo`
injects the selected Manifest and starts Muse in its native yolo mode;
`va muse resume --last` resumes through a fresh launch with freshly resolved
secrets.

Day-one configuration uses `plainfile` + `empty.env`. Point the Harness at
your chosen Backend and Manifest to inject secrets; installer vault wiring
also applies to Muse. Muse owns its account login under the launch account,
so a Service user needs its own login. An injected `META_API_KEY` takes
precedence over Muse's account login (`muse login --help`).

### Kimi

Shipped / auto harness defaults to `kimi --auto` (unattended). Day-one is
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ curl -fsSL https://vaultedagent.com/install.sh | bash
```

Installs `vaulted-agent` and `va`, detects agents on PATH (`claude`, `codex`,
`grok`, `kimi`, `agy`) and `bash`, and can ask for a vault backend + auth mode. Pin:
`grok`, `kimi`, `agy`, `muse`) and `bash`, and can ask for a vault backend + auth mode. Pin:
`VAULTED_AGENT_VERSION=v0.4.22` (or `latest`).

### 2. Wire a vault
Expand All @@ -63,6 +63,8 @@ va codex
va grok
va kimi # --auto; vault inject OPENAI_API_KEY (by provider type); see AGENTS.md
va agy # bare AGY; native permissions, authentication, and conversation args
va muse # bare Muse Code; preserves Muse's configured permissions
va muse --yolo # inject the manifest, then launch Muse in native yolo mode
va bash # secrets-injected shell; extra argv is appended
va bash ./script.sh
```
Expand All @@ -81,6 +83,7 @@ va -m readonly.env.tpl claude # this launch only, against another manifest
va claude --resume <id> # agent args pass through; resume shape is normalized
va agy --continue # AGY arguments pass through unchanged; short form: -c
va agy --conversation <id>
va muse resume --last # native Muse arguments pass through unchanged
va bash # interactive shell with the harness manifest
va bash ./script.sh # same env, run a script; not `va run`
va doctor
Expand Down Expand Up @@ -318,7 +321,7 @@ config file you have edited. Useful flags:
| `--user NAME` | the service account to run agents as; defaults to you (writes `service_user` in defaults.conf when explicit) |
| `--no-link` | skip the default `~/.local/bin` symlink |
| `--no-va` | skip the short `va` alias (default is to install it) |
| `--no-auto-harness` | do not detect claude/codex/grok/kimi/agy/bash or write live harnesses |
| `--no-auto-harness` | do not detect claude/codex/grok/kimi/agy/muse/bash or write live harnesses |
| `--no-setup` | skip interactive vault backend questions |
| `--backend NAME` | `onepassword`, `bitwarden`, `pass`, `sops`, or `skip`. Sets `default_backend` in `defaults.conf` and the summary’s token path (`bws.env` vs `op.env`) |
| `--auth-mode MODE` | `file` (token on disk) or `prompt` (paste each launch; default `file`) |
Expand Down Expand Up @@ -521,7 +524,7 @@ command = claude --permission-mode auto
| `command` | the command line, split on whitespace |
| `arg` | one further argument, verbatim. Repeatable, and the only way to pass one containing a space |

See [Resume sessions](#resume-sessions) above for `va claude|codex|grok|kimi|agy`
See [Resume sessions](#resume-sessions) above for `va claude|codex|grok|kimi|agy|muse`
resume examples. Native CLIs still differ without `va`: Claude/Grok use
`--resume`; Codex uses the `resume` subcommand; Kimi Code uses `--continue` /
`--session` (and accepts `--resume` as an alias). AGY keeps its native
Expand Down
57 changes: 57 additions & 0 deletions docs/issue-103-muse-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Muse Harness — issue #103

Design discussion for [issue #103](https://github.com/JacobStephens2/vaulted-agent/issues/103).
Shared understanding confirmed by Jacob on 2026-09-13. Implemented using the
existing Harness mechanism.

## Settled requirements

- `va muse` launches Muse Code as a normal Harness, following the existing
support model used by `va claude` and the other agent Harnesses.
- Secret delivery uses a fresh launch through `va`. Adding credentials to an
already-running session is outside this issue.
- The default command is bare `muse`, with `workdir = caller` and labels off.
Muse owns its permission settings; the Launcher adds no permission flags.
- Arguments following the Harness name pass through unchanged. In particular,
`va muse --yolo` injects the selected Manifest and launches Muse in its native
yolo mode. Native subcommands such as `va muse resume --last` also pass through.
- The existing Harness, Manifest, Secret value, Manager token, and Child
environment concepts apply; no new domain term is needed so far.

## Integration scope implied by ordinary Harness support

- Ship a Muse sample profile and detect the installed `muse` executable during
installation. Follow existing preservation of user profiles and installer
defaults: plainfile plus `empty.env`, caller directory, and detected binary
directory. Existing installer vault wiring applies to the starter profile.
- Use the generic Harness support for listing, picking, Manifest overrides,
aliases, Backend resolution, Manager token removal, and Service-user re-exec.
- Include Muse in Doctor's existing agent-specific working-directory checks.
- Document the launch and argument examples alongside the other Harnesses.
- Preserve the distinction between installer vault wiring and `va setup`:
the latter currently creates configuration and tells the operator how to
point a Harness at the Manifest; it does not automatically rewire profiles.

## Acceptance and verification

- CLI coverage verifies injection of a synthetic Secret value, absence of
Manager tokens, caller directory, and exact forwarding of `--yolo` and
native resume arguments through a Muse executable stub.
- Installer coverage verifies discovery, starter configuration, and preservation
of an existing Muse profile. Doctor coverage checks Muse's working-directory
behavior.
- A bounded runtime probe through the built Launcher and shipped Muse profile
passed on macOS with Muse 1.1.1. A temporary Manifest provided only the
non-secret `VA_MUSE_PROBE=sentinel-103`; `muse exec --yolo` ran a shell check.
The actual `tool.result` event reported exit code 0 and
`VA_MUSE_INHERITANCE_OK`, proving delivery to Muse's shell tool. This did not
exercise a real third-party credential or authenticated external action.

Local inspection of Muse 1.1.1 confirms bare `muse`, `--yolo`, and native
`resume` support. Its login help also states that `META_API_KEY` takes precedence
over account login; normal Manifest selection continues to determine what is
injected. No Muse-specific authentication configuration is required by this
design.

The current decisions use the established Launcher architecture and do not yet
justify a separate ADR.
3 changes: 2 additions & 1 deletion etc/harnesses.d/README
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ Recognised keys, all optional except command and manifest:
va claude --resume b03ca221-… | va claude resume b03ca221-…
va codex resume 019fabc2-… | va codex --resume 019fabc2-…
va agy --continue | va agy --conversation <uuid>
va muse --yolo | va muse resume --last
va bash | va bash ./script.sh
Shape is normalized per agent: codex wants `resume`; claude/grok want `--resume`.
AGY keeps its native conversation arguments unchanged.
AGY and Muse keep their native arguments unchanged.
`va bash` is a harness (command is always bash). It is not `va run`.

Whitespace around the key, the `=`, and the value is ignored, so align them
Expand Down
10 changes: 10 additions & 0 deletions etc/harnesses.d/muse.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Reached as: va muse
# Bare Muse Code preserves its configured permissions.
# Arguments pass through: va muse --yolo / va muse resume --last

bin = $HOME/.local/bin
backend = plainfile
manifest = empty.env
labels = no
workdir = caller
command = muse
20 changes: 14 additions & 6 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ ALLOW_USER="" # write a sudoers rule for this user
LINK_USER="" # symlink into this user's ~/.local/bin
NO_LINK=0 # skip the default ~/.local/bin symlink
NO_VA=0 # skip the short `va` alias symlink
NO_AUTO_HARNESS=0 # skip detecting claude/codex/grok/kimi/agy/bash
NO_AUTO_HARNESS=0 # skip detecting claude/codex/grok/kimi/agy/muse/bash
NO_SETUP=0 # skip interactive vault backend questions
SHORT_NAME="va" # short alias for vaulted-agent
BACKEND_CHOICE="" # onepassword|bitwarden|pass|sops|plainfile|skip
Expand Down Expand Up @@ -552,6 +552,14 @@ if (( ! NO_AUTO_HARNESS )); then
else
printf ' %-8s not found (skipped)\n' agy
fi
# Muse Code keeps its native permissions; e.g. va muse --yolo opts in per run.
if p="$(find_user_bin muse)"; then
write_auto_harness muse "$p" "muse"
found_any=1
found_agent=1
else
printf ' %-8s not found (skipped)\n' muse
fi
# bash is almost always on PATH; do not treat it as an agent CLI being found.
if p="$(find_user_bin bash)"; then
write_auto_harness bash "$p" "bash"
Expand All @@ -562,13 +570,13 @@ if (( ! NO_AUTO_HARNESS )); then
if (( found_any )); then
printf '\nAuto-harnesses use plainfile + empty.env (no vault secrets yet).\n'
if (( found_agent )); then
printf ' Try: va claude / va codex / va grok / va kimi / va agy / va bash\n'
printf ' Try: va claude / va codex / va grok / va kimi / va agy / va muse / va bash\n'
else
printf ' Try: va bash (secrets-injected shell; extra argv is appended)\n'
fi
fi
if (( ! found_agent )); then
printf ' No claude/codex/grok/kimi/agy found. Install an agent CLI, then re-run install\n'
printf ' No claude/codex/grok/kimi/agy/muse found. Install an agent CLI, then re-run install\n'
printf ' or copy a harnesses.d/*.conf.example and drop the .example suffix.\n'
fi
unset found_any found_agent p
Expand Down Expand Up @@ -665,7 +673,7 @@ ensure_ref_manifest() {
printf ' wrote %s (add VAR=reference lines when ready)\n' "$path"
}

# Personal installs: agent sessions (claude/codex/grok/kimi/agy) are cwd-scoped.
# Personal installs: agent sessions (claude/codex/grok/kimi/agy/muse) are cwd-scoped.
# Ensure live harnesses use workdir=caller so `va grok --resume …` / `va kimi
# --continue` / `va agy --continue` match a normal launch from the same directory.
ensure_workdir_caller() {
Expand Down Expand Up @@ -1137,8 +1145,8 @@ if [[ -z "$REFS_MANIFEST_PATH" ]]; then
printf ' cp %s/harnesses.d/claude.conf.example %s/harnesses.d/claude.conf\n' "$CONFIG" "$CONFIG"
fi
printf ' then run: vaulted-agent (or the short alias: %s)\n' "$SHORT_NAME"
printf ' e.g. %s claude / %s grok / %s kimi / %s agy / %s bash\n' \
"$SHORT_NAME" "$SHORT_NAME" "$SHORT_NAME" "$SHORT_NAME" "$SHORT_NAME"
printf ' e.g. %s claude / %s grok / %s kimi / %s agy / %s muse / %s bash\n' \
"$SHORT_NAME" "$SHORT_NAME" "$SHORT_NAME" "$SHORT_NAME" "$SHORT_NAME" "$SHORT_NAME"
printf ' (or: sudo -u %s %s/vaulted-agent)\n' "$SERVICE_USER" "$PREFIX"
printf '\nTo remove this install later:\n'
printf ' sudo vaulted-agent uninstall\n'
Expand Down
5 changes: 4 additions & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,10 @@ fn workdir_warning(
workdir: Option<&str>,
harness: &str,
) -> Option<String> {
let is_agent = matches!(harness, "claude" | "codex" | "grok" | "kimi" | "agy");
let is_agent = matches!(
harness,
"claude" | "codex" | "grok" | "kimi" | "agy" | "muse"
);
let wd_is_caller = workdir == Some("caller");
match (service_user, wd_is_caller) {
(Some(svc), true) => {
Expand Down
39 changes: 20 additions & 19 deletions tests/cli_doctor_workdir_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,25 @@ fn doctor_warns_when_caller_cwd_is_untraversable() {
}

#[test]
fn doctor_treats_agy_as_a_cwd_scoped_agent() {
let seam = CliSeam::new();
fs::write(seam.config_dir.join("manifests/empty.env"), "\n").unwrap();
fs::write(
seam.config_dir.join("harnesses.d/agy.conf"),
"backend = plainfile\nmanifest = empty.env\ncommand = true\n",
)
.unwrap();
fn doctor_treats_native_agents_as_cwd_scoped() {
for harness in ["agy", "muse"] {
let seam = CliSeam::new();
fs::write(seam.config_dir.join("manifests/empty.env"), "\n").unwrap();
seam.write_harness(
harness,
&format!("backend = plainfile\nmanifest = empty.env\ncommand = {harness}\n"),
);

let out = seam.vaulted_agent().arg("doctor").output().expect("doctor");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
combined.contains("harness: agy")
&& combined.contains("WARN: agent harness without workdir=caller"),
"Doctor must protect AGY's cwd-scoped conversations:\n{combined}"
);
let out = seam.vaulted_agent().arg("doctor").output().expect("doctor");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
combined.contains(&format!("harness: {harness}"))
&& combined.contains("WARN: agent harness without workdir=caller"),
"Doctor must warn about {harness}'s working directory:\n{combined}"
);
}
}
59 changes: 59 additions & 0 deletions tests/cli_launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,65 @@ fn shipped_agy_harness_injects_manifest_and_preserves_native_args() {
assert!(rec.contains("ENV APP_TOKEN"), "{rec}");
}

#[test]
fn shipped_muse_harness_injects_manifest_and_preserves_native_args() {
let seam = CliSeam::new();
let home = seam.root.join("home");
let home_bin = home.join(".local/bin");
fs::create_dir_all(&home_bin).unwrap();
fs::rename(seam.install_stub_agent("muse"), home_bin.join("muse")).unwrap();
fs::write(
seam.config_dir.join("manifests/empty.env"),
"APP_TOKEN=from-manifest\n",
)
.unwrap();
fs::copy(
format!("{}/etc/harnesses.d/muse.conf", env!("CARGO_MANIFEST_DIR")),
seam.config_dir.join("harnesses.d/muse.conf"),
)
.unwrap();

for (args, expected) in [
(vec!["--yolo"], "ARGV: --yolo"),
(vec!["resume", "--last"], "ARGV: resume --last"),
(
vec!["exec", "--yolo", "review this"],
"ARGV: exec --yolo review\\ this",
),
] {
let out = seam
.vaulted_agent()
.env("HOME", &home)
.env("BWS_ACCESS_TOKEN", "synthetic-manager-token")
.env("OP_SERVICE_ACCOUNT_TOKEN", "synthetic-manager-token")
.env("PARENT_ONLY_SECRET", "must-not-appear")
.arg("muse")
.args(args)
.output()
.expect("launch");
assert!(
out.status.success(),
"stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let rec = seam.read_stub_record("muse");
assert_eq!(rec.lines().next(), Some(expected));
assert!(rec.contains("ENV APP_TOKEN=from-manifest\n"), "{rec}");
let cwd = fs::canonicalize(&seam.work_dir).unwrap();
assert!(
rec.contains(&format!("ENV PWD={}\n", cwd.display())),
"{rec}"
);
for excluded in [
"BWS_ACCESS_TOKEN",
"OP_SERVICE_ACCOUNT_TOKEN",
"PARENT_ONLY_SECRET",
] {
assert!(!rec.contains(excluded), "{excluded} leaked: {rec}");
}
}
}

#[test]
fn manifest_override_swaps_which_secrets_reach_the_agent() {
// `va -m other.env claude` — the harness still decides the command, the
Expand Down
Loading
Loading