Skip to content

Commit 30e5dfc

Browse files
authored
feat(gateway): read-only agents.workspace list/get browsing RPCs (#100738)
* feat(gateway): add read-only agents.workspace list/get browsing RPCs * fix(gateway): gate workspace image reads on sniffed magic bytes, not extension * fix(gateway): tighten workspace browsing boundary * docs(gateway): clarify workspace read trust scope * chore(gateway): leave release notes to release flow
1 parent add5c51 commit 30e5dfc

13 files changed

Lines changed: 1157 additions & 143 deletions

File tree

apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5339,6 +5339,170 @@ public struct AgentsFilesSetResult: Codable, Sendable {
53395339
}
53405340
}
53415341

5342+
public struct AgentsWorkspaceEntry: Codable, Sendable {
5343+
public let path: String
5344+
public let name: String
5345+
public let kind: AnyCodable
5346+
public let size: Int?
5347+
public let updatedatms: Int?
5348+
5349+
public init(
5350+
path: String,
5351+
name: String,
5352+
kind: AnyCodable,
5353+
size: Int?,
5354+
updatedatms: Int?)
5355+
{
5356+
self.path = path
5357+
self.name = name
5358+
self.kind = kind
5359+
self.size = size
5360+
self.updatedatms = updatedatms
5361+
}
5362+
5363+
private enum CodingKeys: String, CodingKey {
5364+
case path
5365+
case name
5366+
case kind
5367+
case size
5368+
case updatedatms = "updatedAtMs"
5369+
}
5370+
}
5371+
5372+
public struct AgentsWorkspaceFile: Codable, Sendable {
5373+
public let path: String
5374+
public let name: String
5375+
public let size: Int
5376+
public let updatedatms: Int
5377+
public let mimetype: String
5378+
public let encoding: AnyCodable
5379+
public let content: String
5380+
5381+
public init(
5382+
path: String,
5383+
name: String,
5384+
size: Int,
5385+
updatedatms: Int,
5386+
mimetype: String,
5387+
encoding: AnyCodable,
5388+
content: String)
5389+
{
5390+
self.path = path
5391+
self.name = name
5392+
self.size = size
5393+
self.updatedatms = updatedatms
5394+
self.mimetype = mimetype
5395+
self.encoding = encoding
5396+
self.content = content
5397+
}
5398+
5399+
private enum CodingKeys: String, CodingKey {
5400+
case path
5401+
case name
5402+
case size
5403+
case updatedatms = "updatedAtMs"
5404+
case mimetype = "mimeType"
5405+
case encoding
5406+
case content
5407+
}
5408+
}
5409+
5410+
public struct AgentsWorkspaceListParams: Codable, Sendable {
5411+
public let agentid: String
5412+
public let path: String?
5413+
public let offset: Int?
5414+
public let limit: Int?
5415+
5416+
public init(
5417+
agentid: String,
5418+
path: String?,
5419+
offset: Int?,
5420+
limit: Int?)
5421+
{
5422+
self.agentid = agentid
5423+
self.path = path
5424+
self.offset = offset
5425+
self.limit = limit
5426+
}
5427+
5428+
private enum CodingKeys: String, CodingKey {
5429+
case agentid = "agentId"
5430+
case path
5431+
case offset
5432+
case limit
5433+
}
5434+
}
5435+
5436+
public struct AgentsWorkspaceListResult: Codable, Sendable {
5437+
public let agentid: String
5438+
public let path: String
5439+
public let parentpath: String?
5440+
public let entries: [AgentsWorkspaceEntry]
5441+
public let totalentries: Int
5442+
public let offset: Int
5443+
5444+
public init(
5445+
agentid: String,
5446+
path: String,
5447+
parentpath: String?,
5448+
entries: [AgentsWorkspaceEntry],
5449+
totalentries: Int,
5450+
offset: Int)
5451+
{
5452+
self.agentid = agentid
5453+
self.path = path
5454+
self.parentpath = parentpath
5455+
self.entries = entries
5456+
self.totalentries = totalentries
5457+
self.offset = offset
5458+
}
5459+
5460+
private enum CodingKeys: String, CodingKey {
5461+
case agentid = "agentId"
5462+
case path
5463+
case parentpath = "parentPath"
5464+
case entries
5465+
case totalentries = "totalEntries"
5466+
case offset
5467+
}
5468+
}
5469+
5470+
public struct AgentsWorkspaceGetParams: Codable, Sendable {
5471+
public let agentid: String
5472+
public let path: String
5473+
5474+
public init(
5475+
agentid: String,
5476+
path: String)
5477+
{
5478+
self.agentid = agentid
5479+
self.path = path
5480+
}
5481+
5482+
private enum CodingKeys: String, CodingKey {
5483+
case agentid = "agentId"
5484+
case path
5485+
}
5486+
}
5487+
5488+
public struct AgentsWorkspaceGetResult: Codable, Sendable {
5489+
public let agentid: String
5490+
public let file: AgentsWorkspaceFile
5491+
5492+
public init(
5493+
agentid: String,
5494+
file: AgentsWorkspaceFile)
5495+
{
5496+
self.agentid = agentid
5497+
self.file = file
5498+
}
5499+
5500+
private enum CodingKeys: String, CodingKey {
5501+
case agentid = "agentId"
5502+
case file
5503+
}
5504+
}
5505+
53425506
public struct ArtifactSummary: Codable, Sendable {
53435507
public let id: String
53445508
public let type: String

docs/gateway/protocol.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ methods. Treat this as feature discovery, not a full enumeration of
427427
- `agents.create`, `agents.update`, and `agents.delete` manage agent records and workspace wiring.
428428
- `agents.files.list`, `agents.files.get`, and `agents.files.set` manage the bootstrap workspace files exposed for an agent.
429429
- `audit.list` returns a bounded metadata-only ledger of agent run and tool action events.
430+
- `agents.workspace.list` and `agents.workspace.get` (`operator.read`) expose read-only, paginated browsing of an agent's workspace directory for clients in the trusted operator domain described in [Operator scopes](/gateway/operator-scopes). Requests accept workspace-relative paths only; reads stay confined to the realpathed workspace root (symlink and hardlink escapes rejected), size-capped, and limited to UTF-8 text plus common image types (base64). Responses do not expose the host workspace path. There are no write operations in this namespace.
430431
- `tasks.list`, `tasks.get`, and `tasks.cancel` expose the gateway task ledger to SDK and operator clients. See [Task ledger RPCs](#task-ledger-rpcs) below.
431432
- `artifacts.list`, `artifacts.get`, and `artifacts.download` expose transcript-derived artifact summaries and downloads for an explicit `sessionKey`, `runId`, or `taskId` scope. Run and task queries resolve the owning session server-side and only return transcript media with matching provenance; unsafe or local URL sources return unsupported downloads instead of fetching server-side.
432433
- `environments.list` and `environments.status` expose read-only gateway-local and node environment discovery for SDK clients.

packages/gateway-protocol/src/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ import {
5353
AgentsFilesSetParamsSchema,
5454
type AgentsFilesSetResult,
5555
AgentsFilesSetResultSchema,
56+
type AgentsWorkspaceEntry,
57+
AgentsWorkspaceEntrySchema,
58+
type AgentsWorkspaceFile,
59+
AgentsWorkspaceFileSchema,
60+
type AgentsWorkspaceGetParams,
61+
AgentsWorkspaceGetParamsSchema,
62+
type AgentsWorkspaceGetResult,
63+
AgentsWorkspaceGetResultSchema,
64+
type AgentsWorkspaceListParams,
65+
AgentsWorkspaceListParamsSchema,
66+
type AgentsWorkspaceListResult,
67+
AgentsWorkspaceListResultSchema,
5668
type ArtifactsDownloadParams,
5769
ArtifactsDownloadParamsSchema,
5870
type ArtifactsDownloadResult,
@@ -673,6 +685,12 @@ export const validateAgentsFilesGetParams = lazyCompile<AgentsFilesGetParams>(
673685
export const validateAgentsFilesSetParams = lazyCompile<AgentsFilesSetParams>(
674686
AgentsFilesSetParamsSchema,
675687
);
688+
export const validateAgentsWorkspaceListParams = lazyCompile<AgentsWorkspaceListParams>(
689+
AgentsWorkspaceListParamsSchema,
690+
);
691+
export const validateAgentsWorkspaceGetParams = lazyCompile<AgentsWorkspaceGetParams>(
692+
AgentsWorkspaceGetParamsSchema,
693+
);
676694
export const validateArtifactsListParams =
677695
lazyCompile<ArtifactsListParams>(ArtifactsListParamsSchema);
678696
export const validateArtifactsGetParams = lazyCompile<ArtifactsGetParams>(ArtifactsGetParamsSchema);
@@ -1273,6 +1291,12 @@ export {
12731291
AgentsFilesGetResultSchema,
12741292
AgentsFilesSetParamsSchema,
12751293
AgentsFilesSetResultSchema,
1294+
AgentsWorkspaceEntrySchema,
1295+
AgentsWorkspaceFileSchema,
1296+
AgentsWorkspaceListParamsSchema,
1297+
AgentsWorkspaceListResultSchema,
1298+
AgentsWorkspaceGetParamsSchema,
1299+
AgentsWorkspaceGetResultSchema,
12761300
AgentsListParamsSchema,
12771301
AgentsListResultSchema,
12781302
CommandsListParamsSchema,
@@ -1463,6 +1487,12 @@ export type {
14631487
AgentsFilesGetResult,
14641488
AgentsFilesSetParams,
14651489
AgentsFilesSetResult,
1490+
AgentsWorkspaceEntry,
1491+
AgentsWorkspaceFile,
1492+
AgentsWorkspaceListParams,
1493+
AgentsWorkspaceListResult,
1494+
AgentsWorkspaceGetParams,
1495+
AgentsWorkspaceGetResult,
14661496
SessionFileBrowserEntry,
14671497
SessionFileBrowserResult,
14681498
SessionFileEntry,

packages/gateway-protocol/src/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
export * from "./schema/primitives.js";
88
export * from "./schema/agent.js";
99
export * from "./schema/agents-models-skills.js";
10+
export * from "./schema/agents-workspace.js";
1011
export * from "./schema/artifacts.js";
1112
export * from "./schema/audit.js";
1213
export * from "./schema/channels.js";
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Gateway Protocol schema module defines protocol validation shapes.
2+
import { Type } from "typebox";
3+
import { NonEmptyString } from "./primitives.js";
4+
5+
/**
6+
* Read-only agent workspace browsing schemas.
7+
*
8+
* These contracts back the workspace file browser in operator clients
9+
* (mobile apps, Control UI). The surface is intentionally read-only:
10+
* write/delete/upload stay out of this namespace until a separately
11+
* reviewed mutation contract exists.
12+
*/
13+
14+
/** One file or folder in an agent workspace directory listing. */
15+
export const AgentsWorkspaceEntrySchema = Type.Object(
16+
{
17+
path: NonEmptyString,
18+
name: NonEmptyString,
19+
kind: Type.Union([Type.Literal("file"), Type.Literal("directory")]),
20+
size: Type.Optional(Type.Integer({ minimum: 0 })),
21+
updatedAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
22+
},
23+
{ additionalProperties: false },
24+
);
25+
26+
/** Lists one directory of an agent workspace. */
27+
export const AgentsWorkspaceListParamsSchema = Type.Object(
28+
{
29+
agentId: NonEmptyString,
30+
path: Type.Optional(Type.String()),
31+
offset: Type.Optional(Type.Integer({ minimum: 0 })),
32+
limit: Type.Optional(Type.Integer({ minimum: 1 })),
33+
},
34+
{ additionalProperties: false },
35+
);
36+
37+
/** Paginated directory listing rooted at the agent workspace. */
38+
export const AgentsWorkspaceListResultSchema = Type.Object(
39+
{
40+
agentId: NonEmptyString,
41+
path: Type.String(),
42+
parentPath: Type.Optional(Type.String()),
43+
entries: Type.Array(AgentsWorkspaceEntrySchema),
44+
totalEntries: Type.Integer({ minimum: 0 }),
45+
offset: Type.Integer({ minimum: 0 }),
46+
},
47+
{ additionalProperties: false },
48+
);
49+
50+
/** One workspace file preview payload (UTF-8 text or base64 image). */
51+
export const AgentsWorkspaceFileSchema = Type.Object(
52+
{
53+
path: NonEmptyString,
54+
name: NonEmptyString,
55+
size: Type.Integer({ minimum: 0 }),
56+
updatedAtMs: Type.Integer({ minimum: 0 }),
57+
mimeType: NonEmptyString,
58+
encoding: Type.Union([Type.Literal("utf8"), Type.Literal("base64")]),
59+
content: Type.String(),
60+
},
61+
{ additionalProperties: false },
62+
);
63+
64+
/** Reads one workspace file by workspace-relative path. */
65+
export const AgentsWorkspaceGetParamsSchema = Type.Object(
66+
{
67+
agentId: NonEmptyString,
68+
path: NonEmptyString,
69+
},
70+
{ additionalProperties: false },
71+
);
72+
73+
/** Result for reading one workspace file. */
74+
export const AgentsWorkspaceGetResultSchema = Type.Object(
75+
{
76+
agentId: NonEmptyString,
77+
file: AgentsWorkspaceFileSchema,
78+
},
79+
{ additionalProperties: false },
80+
);

packages/gateway-protocol/src/schema/protocol-schemas.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ import {
7979
ToolsInvokeParamsSchema,
8080
ToolsInvokeResultSchema,
8181
} from "./agents-models-skills.js";
82+
import {
83+
AgentsWorkspaceEntrySchema,
84+
AgentsWorkspaceFileSchema,
85+
AgentsWorkspaceGetParamsSchema,
86+
AgentsWorkspaceGetResultSchema,
87+
AgentsWorkspaceListParamsSchema,
88+
AgentsWorkspaceListResultSchema,
89+
} from "./agents-workspace.js";
8290
import {
8391
ArtifactSummarySchema,
8492
ArtifactsDownloadParamsSchema,
@@ -552,6 +560,12 @@ export const ProtocolSchemas = {
552560
AgentsFilesGetResult: AgentsFilesGetResultSchema,
553561
AgentsFilesSetParams: AgentsFilesSetParamsSchema,
554562
AgentsFilesSetResult: AgentsFilesSetResultSchema,
563+
AgentsWorkspaceEntry: AgentsWorkspaceEntrySchema,
564+
AgentsWorkspaceFile: AgentsWorkspaceFileSchema,
565+
AgentsWorkspaceListParams: AgentsWorkspaceListParamsSchema,
566+
AgentsWorkspaceListResult: AgentsWorkspaceListResultSchema,
567+
AgentsWorkspaceGetParams: AgentsWorkspaceGetParamsSchema,
568+
AgentsWorkspaceGetResult: AgentsWorkspaceGetResultSchema,
555569
ArtifactSummary: ArtifactSummarySchema,
556570
ArtifactsListParams: ArtifactsListParamsSchema,
557571
ArtifactsListResult: ArtifactsListResultSchema,

packages/gateway-protocol/src/schema/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,12 @@ export type AgentsFilesGetParams = SchemaType<"AgentsFilesGetParams">;
205205
export type AgentsFilesGetResult = SchemaType<"AgentsFilesGetResult">;
206206
export type AgentsFilesSetParams = SchemaType<"AgentsFilesSetParams">;
207207
export type AgentsFilesSetResult = SchemaType<"AgentsFilesSetResult">;
208+
export type AgentsWorkspaceEntry = SchemaType<"AgentsWorkspaceEntry">;
209+
export type AgentsWorkspaceFile = SchemaType<"AgentsWorkspaceFile">;
210+
export type AgentsWorkspaceListParams = SchemaType<"AgentsWorkspaceListParams">;
211+
export type AgentsWorkspaceListResult = SchemaType<"AgentsWorkspaceListResult">;
212+
export type AgentsWorkspaceGetParams = SchemaType<"AgentsWorkspaceGetParams">;
213+
export type AgentsWorkspaceGetResult = SchemaType<"AgentsWorkspaceGetResult">;
208214
export type SessionFileKind = SchemaType<"SessionFileKind">;
209215
export type SessionFileRelevance = SchemaType<"SessionFileRelevance">;
210216
export type SessionFileEntry = SchemaType<"SessionFileEntry">;

src/gateway/methods/core-descriptors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ export const CORE_GATEWAY_METHOD_SPECS: readonly CoreGatewayMethodSpec[] = [
254254
{ name: "controlUi.githubPreview", scope: "operator.read" },
255255
// Additive discovery methods append here so older clients keep stable indices.
256256
{ name: "system.info", scope: "operator.read" },
257+
// Workspace contents stay in the documented trusted operator domain, like session and log
258+
// reads. Strong user/tenant isolation requires separate Gateways; see operator-scopes.md.
259+
{ name: "agents.workspace.list", scope: "operator.read" },
260+
{ name: "agents.workspace.get", scope: "operator.read" },
257261
] as const;
258262

259263
const CORE_GATEWAY_METHOD_SPEC_BY_NAME: ReadonlyMap<string, CoreGatewayMethodSpec> = new Map(

src/gateway/server-methods.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ const loadAgentsHandlers = lazyHandlerModule(
7070
() => import("./server-methods/agents.js"),
7171
(module) => module.agentsHandlers,
7272
);
73+
const loadAgentsWorkspaceHandlers = lazyHandlerModule(
74+
() => import("./server-methods/agents-workspace.js"),
75+
(module) => module.agentsWorkspaceHandlers,
76+
);
7377
const loadArtifactsHandlers = lazyHandlerModule(
7478
() => import("./server-methods/artifacts.js"),
7579
(module) => module.artifactsHandlers,
@@ -655,6 +659,10 @@ export const coreGatewayHandlers: GatewayRequestHandlers = {
655659
],
656660
loadHandlers: loadAgentsHandlers,
657661
}),
662+
...createLazyCoreHandlers({
663+
methods: ["agents.workspace.list", "agents.workspace.get"],
664+
loadHandlers: loadAgentsWorkspaceHandlers,
665+
}),
658666
...createLazyCoreHandlers({
659667
methods: ["artifacts.list", "artifacts.get", "artifacts.download"],
660668
loadHandlers: loadArtifactsHandlers,

0 commit comments

Comments
 (0)