feat(web): open a produced file from the conversation
Serve one file at a time out of a Session's workspace under /f on the web transport, and point the conversation's existing file-open affordance at it. Clicking a write/edit/read row's path now opens that file in a browser tab — including from a LAN client, where the Host's system opener is fenced to loopback and answered nothing. - /f/<sessionId>/<segments> in client-connection, behind the same browser-trust fence as /api; realpath confinement, streamed reads, GET/HEAD only, nosniff + no-store. - Script-capable documents carry CSP sandbox: model-authored markup must not be same-origin with /api, where events.mux is a readable GET stream. - ApiProxy.workspaceRootOf answers where a Session's files live without resuming an agent; the client program cannot reach the core services. - The /f URL shape lives in dsh-host-apiproxy/api so both ends share one encoding (client bundles may not value-import another plugin).
This commit is contained in:
@@ -276,6 +276,21 @@ describe('WorkspacesService', () => {
|
||||
await expect(workspaces.openPath('/missing')).rejects.toThrow(/path open failed/)
|
||||
})
|
||||
|
||||
it('addresses a workspace file by URL, and only inside the workspace', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
const session = 's-1' as SessionId
|
||||
// The URL is derived, not fetched: no wire call answers a link.
|
||||
expect(workspaces.fileUrl(session, '/w/alpha', '/w/alpha/out/a b.html')).toBe('/f/s-1/out/a%20b.html')
|
||||
expect(workspaces.fileUrl(session, '/w/alpha', 'out/index.html')).toBe('/f/s-1/out/index.html')
|
||||
// Outside the workspace there is nothing this transport may serve, which
|
||||
// is the signal a caller falls back to openPath on.
|
||||
expect(workspaces.fileUrl(session, '/w/alpha', '/etc/hosts')).toBeUndefined()
|
||||
expect(api.calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('deletes a Workspace or preserves it when the Host rejects deletion', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
|
||||
Reference in New Issue
Block a user