Skip to content

Commit d186bc3

Browse files
Shawclaude
andcommitted
wip: capability router doc and core capabilities test edits
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7fb5ec5 commit d186bc3

2 files changed

Lines changed: 96 additions & 2 deletions

File tree

packages/agent/docs/capability-router-remote-plugins.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,11 @@ GET /v1/capabilities/assets/:moduleId/<asset-path>
367367
```
368368

369369
The capability server resolves that request through `plugin.asset.get` and
370-
returns the decoded asset bytes with the declared content type. Local plugins
371-
should continue to use `bundlePath`.
370+
returns the decoded asset bytes with the declared content type. The RPC result
371+
is decoded before an HTTP response is built: returned asset paths must satisfy
372+
the same safe asset-path rules as `bundlePath`, `contentType` and `integrity`
373+
must not contain response-splitting control characters, and `bodyBase64` must
374+
be valid standard base64. Local plugins should continue to use `bundlePath`.
372375

373376
## Runtime Integration
374377

@@ -649,6 +652,8 @@ Current focused tests cover:
649652
- remote frontend asset path validation before browser import URL creation and
650653
before same-origin asset proxy dispatch,
651654
- remote frontend bundle URL validation before browser import URL exposure,
655+
- remote asset RPC response validation before decoded bytes and content-type
656+
metadata are exposed through the asset proxy,
652657
- browser-facing view registry and `/api/views` metadata for remote absolute
653658
bundle URLs,
654659
- app-shell `DynamicViewLoader` behavior for absolute remote bundle URLs,

packages/core/src/capabilities/index.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,95 @@ describe("capability router", () => {
547547
]);
548548
});
549549

550+
it("routes remote plugin asset reads through the runtime broker", async () => {
551+
const router = new RuntimeBrokerCapabilityRouter({
552+
invokeRuntime: async () => ({
553+
path: "/assets/weather.js",
554+
contentType: "text/javascript",
555+
bodyBase64: Buffer.from("export const weather = true;").toString(
556+
"base64",
557+
),
558+
integrity: "sha256-weather",
559+
}),
560+
});
561+
562+
await expect(
563+
router.plugin.getAsset({
564+
moduleId: "remote-weather",
565+
path: "/assets/weather.js",
566+
}),
567+
).resolves.toEqual({
568+
path: "/assets/weather.js",
569+
contentType: "text/javascript",
570+
bodyBase64: "ZXhwb3J0IGNvbnN0IHdlYXRoZXIgPSB0cnVlOw==",
571+
integrity: "sha256-weather",
572+
});
573+
});
574+
575+
it("rejects remote plugin assets with unsafe returned paths", async () => {
576+
const router = new RuntimeBrokerCapabilityRouter({
577+
invokeRuntime: async () => ({
578+
path: "../secret.js",
579+
contentType: "text/javascript",
580+
bodyBase64: Buffer.from("export default {};").toString("base64"),
581+
}),
582+
});
583+
584+
await expect(
585+
router.plugin.getAsset({
586+
moduleId: "remote-weather",
587+
path: "/assets/weather.js",
588+
}),
589+
).rejects.toMatchObject({
590+
code: "CAPABILITY_DECODE_FAILED",
591+
method: "plugin.asset.get",
592+
message:
593+
"path must not contain empty, current-directory, or parent-directory segments.",
594+
});
595+
});
596+
597+
it("rejects remote plugin assets with unsafe content types", async () => {
598+
const router = new RuntimeBrokerCapabilityRouter({
599+
invokeRuntime: async () => ({
600+
path: "/assets/weather.js",
601+
contentType: "text/javascript\r\nx-injected: yes",
602+
bodyBase64: Buffer.from("export default {};").toString("base64"),
603+
}),
604+
});
605+
606+
await expect(
607+
router.plugin.getAsset({
608+
moduleId: "remote-weather",
609+
path: "/assets/weather.js",
610+
}),
611+
).rejects.toMatchObject({
612+
code: "CAPABILITY_DECODE_FAILED",
613+
method: "plugin.asset.get",
614+
message: "contentType must not contain control characters.",
615+
});
616+
});
617+
618+
it("rejects remote plugin assets with invalid base64 bodies", async () => {
619+
const router = new RuntimeBrokerCapabilityRouter({
620+
invokeRuntime: async () => ({
621+
path: "/assets/weather.js",
622+
contentType: "text/javascript",
623+
bodyBase64: "not base64!",
624+
}),
625+
});
626+
627+
await expect(
628+
router.plugin.getAsset({
629+
moduleId: "remote-weather",
630+
path: "/assets/weather.js",
631+
}),
632+
).rejects.toMatchObject({
633+
code: "CAPABILITY_DECODE_FAILED",
634+
method: "plugin.asset.get",
635+
message: "bodyBase64 must be valid base64.",
636+
});
637+
});
638+
550639
it("rejects remote plugin manifests with empty module identifiers", async () => {
551640
const router = new RuntimeBrokerCapabilityRouter({
552641
invokeRuntime: async () => ({

0 commit comments

Comments
 (0)