Skip to content

Commit f4c5903

Browse files
committed
add workspace schema versioning
1 parent 5b6cac7 commit f4c5903

7 files changed

Lines changed: 56 additions & 35 deletions

File tree

x-pack/platform/plugins/shared/agent_builder/server/services/execution/filesystem/filesystem_service.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,6 @@ describe('FilesystemService', () => {
6060

6161
it('loads persisted workspace files on init when workspaceId is set', async () => {
6262
workspaceClient.load.mockResolvedValueOnce({
63-
workspace_id: 'ws-1',
64-
created_at: '2025-01-01T00:00:00.000Z',
65-
updated_at: '2025-01-01T00:00:00.000Z',
6663
files: {
6764
'/workspace/persisted.txt': {
6865
content: Buffer.from('saved').toString('base64'),

x-pack/platform/plugins/shared/agent_builder/server/services/execution/filesystem/workspace_volume.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,15 @@
55
* 2.0.
66
*/
77

8-
import type { IWorkspaceClient, WorkspaceDocument } from '../../workspaces';
8+
import type { IWorkspaceClient, WorkspaceFile, WorkspaceSnapshot } from '../../workspaces';
99
import { WorkspaceVolume } from './workspace_volume';
1010

1111
const mockWorkspaceClient = (): jest.Mocked<IWorkspaceClient> => ({
1212
load: jest.fn().mockResolvedValue(undefined),
1313
save: jest.fn().mockResolvedValue(undefined),
1414
});
1515

16-
const persistedDoc = (files: WorkspaceDocument['files']): WorkspaceDocument => ({
17-
workspace_id: 'ws-test',
18-
created_at: '2025-01-01T00:00:00.000Z',
19-
updated_at: '2025-01-01T00:00:00.000Z',
20-
files,
21-
});
16+
const persistedDoc = (files: Record<string, WorkspaceFile>): WorkspaceSnapshot => ({ files });
2217

2318
describe('WorkspaceVolume', () => {
2419
describe('getOrCreateWorkspaceId', () => {

x-pack/platform/plugins/shared/agent_builder/server/services/workspaces/client/storage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const storageSettings = {
1818
schema: {
1919
properties: {
2020
workspace_id: types.keyword({}),
21+
schema_version: types.long({}),
2122
created_at: types.date({}),
2223
updated_at: types.date({}),
2324
// Never queried - Disable dynamic mapping to avoid explosions from arbitrary path keys

x-pack/platform/plugins/shared/agent_builder/server/services/workspaces/client/workspace_client.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import type { WorkspaceStorage } from './storage';
99
import { WorkspaceClient } from './workspace_client';
10-
import type { WorkspaceDocument } from '../types';
10+
import { WORKSPACE_SCHEMA_VERSION, type WorkspaceDocument } from '../types';
1111

1212
const createMockStorage = () => {
1313
const get = jest.fn();
@@ -21,23 +21,26 @@ const createMockStorage = () => {
2121

2222
describe('WorkspaceClient', () => {
2323
describe('load', () => {
24-
it('returns the document when it exists', async () => {
24+
it('returns the workspace snapshot when the document exists', async () => {
2525
const { storage, get } = createMockStorage();
26+
const files = {
27+
'/workspace/a.txt': {
28+
content: 'aGVsbG8=',
29+
mode: 0o644,
30+
mtime: '2025-01-01T00:00:00.000Z',
31+
},
32+
};
2633
const doc: WorkspaceDocument = {
2734
workspace_id: 'ws-1',
35+
schema_version: WORKSPACE_SCHEMA_VERSION,
2836
created_at: '2025-01-01T00:00:00.000Z',
2937
updated_at: '2025-01-01T00:00:00.000Z',
30-
files: {
31-
'/workspace/a.txt': {
32-
content: 'aGVsbG8=',
33-
mode: 0o644,
34-
mtime: '2025-01-01T00:00:00.000Z',
35-
},
36-
},
38+
files,
3739
};
3840
get.mockResolvedValue({ _source: doc });
3941
const client = new WorkspaceClient({ storage });
40-
expect(await client.load('ws-1')).toEqual(doc);
42+
// load() returns the public snapshot shape (no schema_version/timestamps).
43+
expect(await client.load('ws-1')).toEqual({ files });
4144
expect(get).toHaveBeenCalledWith({ id: 'ws-1' });
4245
});
4346

@@ -75,6 +78,7 @@ describe('WorkspaceClient', () => {
7578
const arg = index.mock.calls[0][0];
7679
expect(arg.id).toBe('ws-2');
7780
expect(arg.document.workspace_id).toBe('ws-2');
81+
expect(arg.document.schema_version).toBe(WORKSPACE_SCHEMA_VERSION);
7882
expect(arg.document.files).toEqual(files);
7983
expect(typeof arg.document.updated_at).toBe('string');
8084
expect(typeof arg.document.created_at).toBe('string');
@@ -86,6 +90,7 @@ describe('WorkspaceClient', () => {
8690
get.mockResolvedValue({
8791
_source: {
8892
workspace_id: 'ws-3',
93+
schema_version: WORKSPACE_SCHEMA_VERSION,
8994
created_at: originalCreated,
9095
updated_at: '2024-12-25T00:00:00.000Z',
9196
files: {},

x-pack/platform/plugins/shared/agent_builder/server/services/workspaces/client/workspace_client.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
*/
77

88
import type { WorkspaceStorage } from './storage';
9-
import type { WorkspaceDocument, WorkspaceFile } from '../types';
9+
import {
10+
WORKSPACE_SCHEMA_VERSION,
11+
type WorkspaceDocument,
12+
type WorkspaceFile,
13+
type WorkspaceSnapshot,
14+
} from '../types';
1015

1116
export interface IWorkspaceClient {
12-
load(workspaceId: string): Promise<WorkspaceDocument | undefined>;
17+
load(workspaceId: string): Promise<WorkspaceSnapshot | undefined>;
1318
save(workspaceId: string, files: Record<string, WorkspaceFile>): Promise<void>;
1419
}
1520

@@ -20,27 +25,35 @@ export class WorkspaceClient implements IWorkspaceClient {
2025
this.storage = storage;
2126
}
2227

23-
async load(workspaceId: string): Promise<WorkspaceDocument | undefined> {
24-
try {
25-
const response = await this.storage.getClient().get({ id: workspaceId });
26-
return response._source as WorkspaceDocument | undefined;
27-
} catch (err) {
28-
if ((err as { statusCode?: number }).statusCode === 404) {
29-
return undefined;
30-
}
31-
throw err;
32-
}
28+
async load(workspaceId: string): Promise<WorkspaceSnapshot | undefined> {
29+
const doc = await this.loadDocument(workspaceId);
30+
if (!doc) return undefined;
31+
return { files: doc.files };
3332
}
3433

3534
async save(workspaceId: string, files: Record<string, WorkspaceFile>): Promise<void> {
3635
const now = new Date().toISOString();
37-
const existing = await this.load(workspaceId);
36+
const existing = await this.loadDocument(workspaceId);
3837
const document: WorkspaceDocument = {
3938
workspace_id: workspaceId,
39+
schema_version: WORKSPACE_SCHEMA_VERSION,
4040
created_at: existing?.created_at ?? now,
4141
updated_at: now,
4242
files,
4343
};
4444
await this.storage.getClient().index({ id: workspaceId, document });
4545
}
46+
47+
/** Internal — full document fetch including metadata. */
48+
private async loadDocument(workspaceId: string): Promise<WorkspaceDocument | undefined> {
49+
try {
50+
const response = await this.storage.getClient().get({ id: workspaceId });
51+
return response._source as WorkspaceDocument | undefined;
52+
} catch (err) {
53+
if ((err as { statusCode?: number }).statusCode === 404) {
54+
return undefined;
55+
}
56+
throw err;
57+
}
58+
}
4659
}

x-pack/platform/plugins/shared/agent_builder/server/services/workspaces/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77

88
export { WorkspaceClient, type IWorkspaceClient } from './client/workspace_client';
99
export { createStorage as createWorkspaceStorage, workspaceIndexName } from './client/storage';
10-
export type { WorkspaceDocument, WorkspaceFile } from './types';
10+
export type { WorkspaceFile, WorkspaceSnapshot } from './types';

x-pack/platform/plugins/shared/agent_builder/server/services/workspaces/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,22 @@ export interface WorkspaceFile {
1616
}
1717

1818
/**
19-
* A workspace document persisted in `chatSystemIndex('workspaces')`.
19+
* Public-facing view of a loaded workspace
20+
*/
21+
export interface WorkspaceSnapshot {
22+
files: Record<string, WorkspaceFile>;
23+
}
24+
25+
/**
26+
* Internal — the ES document shape persisted in the workspaces index.
2027
* `files` keys are absolute paths within `/workspace` (e.g. `/workspace/notes.txt`).
2128
*/
2229
export interface WorkspaceDocument {
2330
workspace_id: string;
31+
schema_version: number;
2432
created_at: string;
2533
updated_at: string;
2634
files: Record<string, WorkspaceFile>;
2735
}
36+
37+
export const WORKSPACE_SCHEMA_VERSION = 1;

0 commit comments

Comments
 (0)