feat(missions): install the skills door, with a credential it is safe to leave

The capability has been built and undeployed since `88eef99d4`:
`claude_cli` accepts `mcp_config` and passes `--mcp-config
--strict-mcp-config`, so Claude Code's own MCP client can reach our
skills server. What was missing was the config document and, underneath
it, a credential that could be left in a container an untrusted agent
reads.

Now both halves happen together — the document goes in, and the daemon is
told to pass it — because doing one without the other leaves a door
installed and unreachable, which looks exactly like a door nobody walked
through. That is the same shape as the hooks that shipped installed and
inert three bugs running.

The API origin defaults to our own `HOSTNAME` rather than a container
name. Mission containers share `clawmates_core` with the server, and the
server's name differs between deployments (`clawmates-server-1` locally,
`clawmates_server_1` on gw-04); docker's embedded DNS resolves a
container id on a user-defined network, so this is self-configuring.
Measured from a sibling container: both the id and the name return 200.

`--allowedTools` is deliberately NOT touched. The provider passes it only
when `tools` is set and the seed already sets it — without it `claude -p`
stops mid-turn asking for write permission. Whether MCP tools also need
naming there is undocumented in anything we control, and the daemon
exposes no config read to merge into the list safely; overwriting it
would take `Write` and `Bash` from every mission agent, and that failure
would look like agents that stopped working rather than a config that was
replaced. So the question gets answered by running a mission with the
door installed. Guessing is how the last three defects in this file got in.

Every failure degrades to "no door", never to a failed launch.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9
This commit is contained in:
Omar Sobh
2026-08-21 11:47:13 -07:00
co-authored by Claude Opus 5
parent 2668191e30
commit 73f5d71c55
4 changed files with 183 additions and 3 deletions
+3 -3
View File
@@ -212,13 +212,13 @@ pub fn install_command(dir: &str) -> String {
"mkdir -p {dir} && rm -f {dir}/tools.jsonl \
&& printf '%s' {script} > {dir}/tap.sh && chmod +x {dir}/tap.sh",
dir = dir,
script = q(&hook_script(dir)),
script = shell_quote(&hook_script(dir)),
)
}
/// Write the composed settings document.
pub fn settings_command(path: &str, settings: &Value) -> String {
format!("printf '%s' {} > {path}", q(&settings.to_string()))
format!("printf '%s' {} > {path}", shell_quote(&settings.to_string()))
}
/// Parse a drained tap.
@@ -266,7 +266,7 @@ pub fn parse(raw: &str) -> Vec<Observed> {
/// Single-quote for `sh`. Local copy, same rule as the stop gate's — these two
/// modules deliberately share no code, so neither can break the other.
fn q(s: &str) -> String {
pub fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}