Skip to content

Serving resources

This content is for the 0.1 version. Switch to the latest version for up-to-date documentation.

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.

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.

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.

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.

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.