Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
0662458
docs(seller-tool): stage-0 contract — README, manifest schema, comman…
Sep 9, 2026
f0c0fbf
docs(seller-tool): stage-0 integration survey and token/grant contract
Sep 9, 2026
ab33c5b
docs(seller-tool): stage-0 paper walks A (file CLI) and B (tenant HTTP)
Sep 9, 2026
1b3ec20
docs(seller-tool): stage-0 test entrypoints, evidence layout and gaps…
Sep 9, 2026
d6a0e5a
docs(seller-tool): F3 + F1/F2 survey corrections in 01
Sep 9, 2026
c46677a
docs(seller-tool): F1/F2/F6 rewrite of the token/grant contract
Sep 9, 2026
b378de5
docs(seller-tool): F4/F5/F6 fixes across manifest, mapping, walk A, t…
Sep 9, 2026
088d82e
feat(tool-kit): seller-level tool holder, fake vendor and MCP bridge
Sep 9, 2026
fc7abc5
test(tool-kit): 32 runnable checks over real processes and sockets
Sep 9, 2026
9a0bd57
build(tool-kit): Linux demo image, crate buildable standalone
Sep 9, 2026
c4d02ba
build(tool-kit): workspace lock entry and a Linux image for the five …
Sep 9, 2026
8a3546c
refactor(tool-kit): one directory per job socket
Sep 9, 2026
2041c93
fix(tool-kit): the image default command stops pretending to be a hel…
Sep 9, 2026
edc56bd
test(tool-kit): container demonstration script
Sep 9, 2026
a5e6880
docs(tool-kit): evidence from the container run, 27/27
Sep 9, 2026
3418a85
docs(seller-tool): mark the withdrawn lifecycle model in 00 and 04
Sep 9, 2026
7d4286b
docs(seller-tool): annotate remaining withdrawn passages
Sep 9, 2026
e565ef9
docs(evidence): explain the duplicate 2026-09-09 bundle
Sep 9, 2026
255e957
docs(evidence): retract a guessed cause, state what is actually knowable
Sep 9, 2026
c936609
docs(seller-tool): F5 - retain custody and lifecycle only, drop grant…
Sep 10, 2026
9385031
build(tool-kit): F4 part 1 - pin by digest, build locked, retain raw …
Sep 10, 2026
11e6dee
WIP(tool-kit): F1 partial - host-side credential scan, NOT RUN, HAS A…
Sep 10, 2026
a0cc31d
docs(handoff): portable handoff for continuation off this machine
Sep 10, 2026
a489658
fix(tool-kit): F2 - race-safe, no-follow file consumption
pmilic021 Sep 10, 2026
9e0c6a5
fix(tool-kit): F1 and F3 demo repairs, F4 build receipt
pmilic021 Sep 10, 2026
6284789
feat(tool-kit): seller-tool onboarding skill and config templates
pmilic021 Sep 10, 2026
9200598
docs(seller-tool): continuation handoff and F2/F4 status
pmilic021 Sep 10, 2026
23a4eb1
docs(evidence): post-repair demo run 39/39, supersede pre-repair bundles
pmilic021 Sep 10, 2026
3931a12
docs(seller-tool): record browser-auth decision - not supported for now
pmilic021 Sep 10, 2026
722ce47
docs(seller-tool): detailed production-integration plan (09)
pmilic021 Sep 10, 2026
9a64fd2
docs(evidence): replace offline bundle with digest-pinned run 39/39
pmilic021 Sep 10, 2026
f00cedd
docs(seller-tool): routing decision tree (10), skill as the router
pmilic021 Sep 11, 2026
472b3d8
docs(seller-tool): correct deferred vs manual-setup in skill and routing
pmilic021 Sep 11, 2026
1ccc747
docs(seller-tool): name the routes instead of numbering rungs
pmilic021 Sep 11, 2026
d9873d7
feat(tool-kit): Proxy swap mechanism - transport shim + synthetic demo
pmilic021 Sep 11, 2026
b705a4e
docs(seller-tool): Public custom-image steps, unsupported-route print…
pmilic021 Sep 11, 2026
9d1c14d
docs(seller-tool): holder runs in its own container; GitHub chosen fo…
pmilic021 Sep 11, 2026
ecdfb09
docs(handoff): record the agreed next step - Proxy swap wiring, then …
pmilic021 Sep 11, 2026
00c62a4
feat(tool-kit): the bridge speaks Streamable HTTP - SSE, session id, …
pmilic021 Sep 11, 2026
e1bbb0e
feat(seller-exec): Proxy swap - offer a vendor MCP server through the…
pmilic021 Sep 12, 2026
ffdc31b
docs(handoff): Proxy swap gates closed - image builds, bridge in imag…
pmilic021 Sep 14, 2026
7c6d43c
feat(seller-exec): Proxy swap accepted against GitHub - live tests, a…
pmilic021 Sep 14, 2026
5883c79
docs(evidence): GitHub acceptance bundle for the Proxy swap route, 20…
pmilic021 Sep 14, 2026
a6a9180
feat(seller-node): Holder route wired into the daemon - held tool con…
pmilic021 Sep 14, 2026
4be07a8
docs(evidence): Holder route live proof through the daemon code, 2026…
pmilic021 Sep 14, 2026
a91df8a
feat(seller-node): several held tools per seat - one holder, one sock…
pmilic021 Sep 14, 2026
85e2477
docs(evidence): two held tools through the daemon code, 2026-09-14
pmilic021 Sep 14, 2026
4051fdd
Merge MakePrisms/main (1569010) into feat/seller-tool-onboarding
pmilic021 Sep 14, 2026
2f92830
docs(handoff): the branch is pushed and under review as MakePrisms/ma…
pmilic021 Sep 14, 2026
d51f136
fix(tool-kit): bind holder connections to their attachment, refuse FI…
pmilic021 Sep 15, 2026
5a48f53
fix(core): scope the MCP swap to Authorization, compare redirect orig…
pmilic021 Sep 15, 2026
3fd1564
fix(core): own a failed holder start, confirm cleanup before marking …
pmilic021 Sep 15, 2026
367e899
docs(seller-tool): record review round 2, state the swap scope and th…
pmilic021 Sep 15, 2026
a0368ce
fix(core): own a docker call across a dropped future, treat an unknow…
pmilic021 Sep 15, 2026
c015c66
fix(tool-kit): publish under the attachment lock, release a bridge wo…
pmilic021 Sep 15, 2026
5ed3b08
fix(core): reconcile stale holders by a marker file, not by the curre…
pmilic021 Sep 15, 2026
5d74245
fix(core): an attach the holder refused is not taken back
pmilic021 Sep 16, 2026
d900aa8
docs(handoff): review round 4 - green light, the duplicate-attach fol…
pmilic021 Sep 16, 2026
71d3b42
Merge remote-tracking branch 'upstream/main' into feat/seller-tool-on…
pmilic021 Sep 16, 2026
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
327 changes: 327 additions & 0 deletions .claude/skills/seller-tool-onboarding/SKILL.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
# admitted .scratch/ into the spike tree)
.scratch/
*.jsonl
# Evidence bundles are the exception: the raw MCP request/response captures ARE the evidence,
# and a summary PASS line is not a substitute for the observation it summarises. Without this
# exception the *.jsonl rule above silently dropped every transcript the demo recorded
# (advisor F4). Sanitised by construction — no credential value passes through these surfaces.
!evidence/**/*.jsonl

# The host-side credential scan (advisor F1) writes a multi-MB tar of the captured job-container
# filesystem, plus its stderr. The scan SUMMARY (evidence/**/job-*-fs-scan*.txt) and the
# transcript hashes in manifest.json are the evidence; the raw tar is a large intermediate. Keep
# it out of the repository.
evidence/**/*.tar
evidence/**/*.tar.err

# Environment / secrets
.env
Expand Down
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"crates/maxplayer-evals",
"crates/maxplayer",
"crates/maxplayer-relay-write-policy",
"crates/maxplayer-tool-kit",
]
# maxplayer-desktop pulls egui's native deps (wayland/x11/fontconfig); keep it out of
# default builds so headless boxes and CI don't inherit them. Build with -p maxplayer-desktop.
Expand All @@ -13,6 +14,7 @@ default-members = [
"crates/maxplayer-evals",
"crates/maxplayer",
"crates/maxplayer-relay-write-policy",
"crates/maxplayer-tool-kit",
]
resolver = "3"

Expand Down
278 changes: 261 additions & 17 deletions crates/maxplayer-core/src/credential_proxy.rs

Large diffs are not rendered by default.

19 changes: 16 additions & 3 deletions crates/maxplayer-core/src/delivery_orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,14 @@ pub struct Phase1Inputs {
/// gives the agent exactly these (with their values read from the container environment) plus the
/// runtime baseline ([`AGENT_ENV_BASELINE`]) and the delivery identity, and nothing else.
pub agent_env_names: Vec<String>,
/// The MCP servers the host minted for this launch — the Proxy swap vendor tools, one entry per
/// `[sandbox] mcp_tools`, each carrying only a per-job placeholder and the proxy's address
/// ([`crate::seller_exec::PreparedLaunch::mcp_servers`]). The orchestrator attaches them to the
/// agent's session as they are; inside the container the policy is pass-through and mints
/// nothing, so this is the only way they reach the session. Carries NO secret. Optional and
/// empty by default, so an inputs file written by a host without this field still parses.
#[serde(default)]
pub mcp_servers: Vec<crate::driver::McpServer>,
/// The delivery remote the orchestrator pushes to (the seller's `git_remote`).
pub relay_url: String,
/// How the push token is obtained. See [`PushTokenSource`] for who can read it and when.
Expand Down Expand Up @@ -549,8 +557,8 @@ fn drive_acp_agent(
workdir: &Path,
) -> Result<AgentOutcome, OrchestratorError> {
use crate::seller_exec::{
AgentRunTimeout, ExecError, SandboxPolicy, run_agent_job_with_env, run_agent_with_retry,
unified_job_timeout,
AgentRunTimeout, ExecError, JobAttachments, SandboxPolicy, run_agent_job_in_env,
run_agent_with_retry, unified_job_timeout,
};
let identity = DeliveryAgentIdentity::for_seller(&inputs.seller_pubkey_hex);
let env = agent_env_allowlist(&inputs.agent_env_names, &identity, |key| {
Expand All @@ -567,14 +575,18 @@ fn drive_acp_agent(
unix_now,
|_attempt| {
let timeout = unified_job_timeout(inputs.deadline_unix, unix_now());
run_agent_job_with_env(
// The host's MCP server list rides along: the pass-through policy here mints none, and
// the vendor tools were minted when the host prepared this container.
run_agent_job_in_env(
&inputs.agent_argv,
&policy,
&inputs.prompt,
workdir,
&identity,
AgentRunTimeout::JobDeadline(timeout),
Some(env.clone()),
// Servers only: the HOST mounted this container, so there is nothing to mount here.
JobAttachments { mcp_servers: inputs.mcp_servers.clone(), extra_mounts: Vec::new() },
)
},
));
Expand Down Expand Up @@ -1729,6 +1741,7 @@ mod tests {
"ANTHROPIC_API_KEY".to_owned(),
"ANTHROPIC_BASE_URL".to_owned(),
],
mcp_servers: Vec::new(),
relay_url: "ext::sh -c evil".to_owned(),
push_token,
handoff_nonce: NONCE.to_owned(),
Expand Down
161 changes: 159 additions & 2 deletions crates/maxplayer-core/src/driver/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,90 @@ pub struct SessionConfig {
pub env: Vec<(String, String)>,
}

/// One MCP server for the session (ACP `session/new` → `mcpServers[]`). Two wire shapes, and the
/// difference between them is the `type` key:
///
/// * [`Self::Stdio`] carries NO `type` key. The adapter the sandbox image bakes (`claude-agent-acp`
/// 0.67.0, `dist/acp-agent.js`, the `mcpServers` loop in `newSession`) treats an entry without
/// `type` as a stdio server, and an entry with any `type` other than `http`/`sse` as unknown — it
/// DROPS that entry. So a stdio entry must never say `type: "stdio"`; the absence is the tag.
/// * [`Self::Http`] carries `type: "http"`, a `url`, and `headers`.
///
/// Both shapes are read off that adapter's own mapping code, not guessed from the spec text. The
/// seller's job path attached no MCP server at all before the Proxy swap route, so the old
/// `{ name, command: [...] }` shape here was never on a wire; it did not match what any adapter
/// reads and is gone.
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(untagged)]
pub enum McpServer {
Stdio(McpServerStdio),
Http(McpServerHttp),
}

impl McpServer {
/// The server name the agent addresses tools under, whichever shape it is.
pub fn name(&self) -> &str {
match self {
Self::Stdio(server) => &server.name,
Self::Http(server) => &server.name,
}
}

/// Whether any value this entry hands the agent — the command, an argument, an env value, the
/// URL, a header value — contains `needle`. The sandbox launch asks this about the proxy's
/// docker alias to decide whether the container needs that alias resolved.
pub fn references(&self, needle: &str) -> bool {
match self {
Self::Stdio(server) => {
server.command.contains(needle)
|| server.args.iter().any(|arg| arg.contains(needle))
|| server.env.iter().any(|pair| pair.value.contains(needle))
}
Self::Http(server) => {
server.url.contains(needle) || server.headers.iter().any(|pair| pair.value.contains(needle))
}
}
}
}

/// A stdio MCP server: the agent spawns `command args…` with `env` added to the child's
/// environment and speaks JSON-RPC over its stdin/stdout.
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct McpServerStdio {
pub name: String,
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: Vec<EnvVariable>,
}

/// A Streamable-HTTP MCP server the agent's own MCP client connects to at `url`, sending `headers`
/// on every request.
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct McpServer {
pub struct McpServerHttp {
/// Always `"http"`. A field rather than a serde tag so the stdio variant can carry NO `type`
/// key at all (see [`McpServer`]).
#[serde(rename = "type")]
pub transport: HttpTransport,
pub name: String,
pub command: Vec<String>,
pub url: String,
#[serde(default)]
pub headers: Vec<EnvVariable>,
}

/// The one value [`McpServerHttp::transport`] takes.
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum HttpTransport {
Http,
}

/// A `{ name, value }` pair, the ACP encoding of one environment variable or one HTTP header.
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct EnvVariable {
pub name: String,
pub value: String,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
Expand Down Expand Up @@ -457,3 +537,80 @@ mod usage_tests {
assert_eq!(usage.total_tokens(), Some(8));
}
}

#[cfg(test)]
mod mcp_server_wire_tests {
use super::*;
use serde_json::json;

// The adapter drops a stdio entry that carries a `type` key (`claude-agent-acp` 0.67.0 reads
// `"type" in server` before anything else), so the stdio shape must serialize with none.
#[test]
fn a_stdio_server_serializes_with_no_type_key() {
let server = McpServer::Stdio(McpServerStdio {
name: "github".into(),
command: "/usr/local/bin/mcp-http-bridge".into(),
args: vec!["--proxy-url".into(), "http://host.docker.internal:9100".into()],
env: vec![EnvVariable {
name: "TOOL_PLACEHOLDER".into(),
value: "ph".into(),
}],
});
let wire = serde_json::to_value(&server).expect("encode");
assert_eq!(
wire,
json!({
"name": "github",
"command": "/usr/local/bin/mcp-http-bridge",
"args": ["--proxy-url", "http://host.docker.internal:9100"],
"env": [{"name": "TOOL_PLACEHOLDER", "value": "ph"}]
})
);
assert!(wire.get("type").is_none(), "a type key makes the adapter drop the entry");
let back: McpServer = serde_json::from_value(wire).expect("decode");
assert_eq!(back, server);
}

#[test]
fn an_http_server_serializes_with_type_http() {
let server = McpServer::Http(McpServerHttp {
transport: HttpTransport::Http,
name: "github".into(),
url: "http://host.docker.internal:9100/mcp/".into(),
headers: vec![EnvVariable {
name: "Authorization".into(),
value: "Bearer ph".into(),
}],
});
let wire = serde_json::to_value(&server).expect("encode");
assert_eq!(
wire,
json!({
"type": "http",
"name": "github",
"url": "http://host.docker.internal:9100/mcp/",
"headers": [{"name": "Authorization", "value": "Bearer ph"}]
})
);
let back: McpServer = serde_json::from_value(wire).expect("decode");
assert_eq!(back, server);
}

// The session config puts the list under the camelCase key the adapter reads.
#[test]
fn the_session_config_carries_the_servers_under_mcp_servers() {
let cfg = SessionConfig {
cwd: "/work".into(),
mcp_servers: vec![McpServer::Stdio(McpServerStdio {
name: "t".into(),
command: "c".into(),
args: Vec::new(),
env: Vec::new(),
})],
env: Vec::new(),
};
let wire = serde_json::to_value(&cfg).expect("encode");
assert_eq!(wire["mcpServers"][0]["name"], json!("t"));
assert_eq!(wire["mcpServers"][0]["args"], json!([]));
}
}
7 changes: 4 additions & 3 deletions crates/maxplayer-core/src/driver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ use std::fmt::{self, Display};
pub use crate::event::RuntimeId;

pub use acp::{
Artifact, Caps, ContentBlock, ExtMethod, Initialize, InitializeResult, McpServer,
PermissionOutcome, PermissionRequest, PromptTurn, SessionConfig, SessionId, SessionUpdate,
StopReason, UpdateStream,
Artifact, Caps, ContentBlock, EnvVariable, ExtMethod, HttpTransport, Initialize,
InitializeResult, McpServer, McpServerHttp, McpServerStdio, PermissionOutcome,
PermissionRequest, PromptTurn, SessionConfig, SessionId, SessionUpdate, StopReason,
UpdateStream,
};
#[cfg(feature = "acp")]
pub use acp_driver::{AcpDriver, AgentCommand};
Expand Down
Loading
Loading