Serving resources
The protocol runs in both directions. A host may ask the client to resolve a file — read it, write it, list a directory — because the client is sometimes the only thing that can see the workspace.
That makes your client a filesystem for someone else’s agent, which is why the default is to refuse everything.
The default
Section titled “The default”final client = AhpConnection(transport);// resourceHost: const DenyingResourceHost()DenyingResourceHost answers every method with null, which the peer turns
into a JSON-RPC “method not found”. The host is never left waiting, and no
client-side path is ever exposed.
Doing nothing is a safe default here, which is not true of most defaults.
Opening one method
Section titled “Opening one method”DenyingResourceHost is deliberately extendable rather than final. Override
the one method you mean to serve and inherit the refusal for the other seven:
class ReadOnlyWorkspace extends DenyingResourceHost { const ReadOnlyWorkspace(this.root);
final Directory root;
@override Future<ResourceReadResult?> read(ResourceReadParams params) async { final file = _resolveWithin(root, params.resource); if (file == null) return null; // outside the root: refuse return ResourceReadResult(content: await file.readAsString()); }}final client = AhpConnection(transport, resourceHost: ReadOnlyWorkspace(root));Implementing the interface directly means writing all eight methods, and the risk there is not effort — it is that an accidental opening looks exactly like an intentional one in review.
Containment
Section titled “Containment”The host supplies the URI. Nothing stops it naming ../../.ssh/id_rsa, so
resolve and check before opening:
File? _resolveWithin(Directory root, String resource) { final path = p.normalize(p.join(root.path, Uri.parse(resource).path)); if (!p.isWithin(root.path, path)) return null; return File(path);}Normalize first, then test containment. Testing the raw string for .. is the
version of this check that gets bypassed.
The eight methods
Section titled “The eight methods”| Method | |
|---|---|
read |
Read a file’s contents |
write |
Create or overwrite |
list |
Enumerate a directory |
copy |
Duplicate |
delete |
Remove |
move |
Rename or relocate |
resolve |
Turn a relative reference into an absolute URI |
mkdir |
Create a directory |
Returning null from any of them declines that one call. It is a per-call
answer, not a permanent capability switch — so a host asking again later gets
asked again, and you can decline based on what was asked.