shennian 0.4.30 → 0.4.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/publish-build-manifest.json +52 -37
  2. package/dist/src/agents/acp/adapter.d.ts +5 -1
  3. package/dist/src/agents/acp/adapter.js +2 -2
  4. package/dist/src/agents/acp/client-services.d.ts +2 -1
  5. package/dist/src/agents/acp/client-services.js +2 -2
  6. package/dist/src/agents/acp/discovery.js +1 -1
  7. package/dist/src/agents/acp/process-client.js +1 -1
  8. package/dist/src/agents/claude.d.ts +1 -0
  9. package/dist/src/agents/claude.js +5 -5
  10. package/dist/src/agents/codex.js +3 -3
  11. package/dist/src/agents/command-spec.d.ts +1 -0
  12. package/dist/src/agents/command-spec.js +3 -3
  13. package/dist/src/agents/model-registry/service.js +1 -1
  14. package/dist/src/commands/daemon-instance-lock.d.ts +21 -0
  15. package/dist/src/commands/daemon-instance-lock.js +1 -0
  16. package/dist/src/commands/daemon.d.ts +2 -2
  17. package/dist/src/commands/daemon.js +6 -6
  18. package/dist/src/index.js +3 -3
  19. package/dist/src/relay/client.d.ts +4 -0
  20. package/dist/src/relay/client.js +1 -1
  21. package/dist/src/room/activation-service.d.ts +2 -0
  22. package/dist/src/room/activation-service.js +4 -4
  23. package/dist/src/room/managed-room-binding.d.ts +2 -1
  24. package/dist/src/room/managed-room-binding.js +1 -1
  25. package/dist/src/room/room-binding-client.js +1 -1
  26. package/dist/src/session/handlers/chat.js +2 -2
  27. package/dist/src/session/handlers/project-fs.d.ts +11 -0
  28. package/dist/src/session/handlers/project-fs.js +1 -0
  29. package/dist/src/session/handlers/room-managed.js +1 -1
  30. package/dist/src/session/manager.d.ts +6 -1
  31. package/dist/src/session/manager.js +2 -2
  32. package/dist/src/session/project-fs-store.d.ts +43 -0
  33. package/dist/src/session/project-fs-store.js +1 -0
  34. package/dist/src/session/queue.js +4 -4
  35. package/node_modules/@shennian/wire/dist/index.d.ts +1 -0
  36. package/node_modules/@shennian/wire/dist/index.js +1 -0
  37. package/node_modules/@shennian/wire/dist/methods.d.ts +3 -1
  38. package/node_modules/@shennian/wire/dist/methods.js +2 -0
  39. package/node_modules/@shennian/wire/dist/project-fs.d.ts +146 -0
  40. package/node_modules/@shennian/wire/dist/project-fs.js +462 -0
  41. package/node_modules/@shennian/wire/dist/project.d.ts +2 -0
  42. package/node_modules/@shennian/wire/dist/project.js +2 -0
  43. package/node_modules/@shennian/wire/dist/session-sync.d.ts +8 -1
  44. package/node_modules/@shennian/wire/dist/session-sync.js +1 -0
  45. package/package.json +1 -1
@@ -0,0 +1,146 @@
1
+ export declare const PROJECT_FS_OPAQUE_CAPABILITY: "project_fs_opaque_v1";
2
+ export type ProjectFsOpaqueCapability = typeof PROJECT_FS_OPAQUE_CAPABILITY;
3
+ export declare const PROJECT_FS_NEGOTIATION_METHOD: "project.fs.negotiate";
4
+ export declare const PROJECT_FS_SENSITIVE_AUTHORITY_VERSION: 1;
5
+ export type ProjectFsNegotiationChallenge = {
6
+ version: typeof PROJECT_FS_SENSITIVE_AUTHORITY_VERSION;
7
+ nonce: string;
8
+ daemonInstallationId: string;
9
+ };
10
+ export type ProjectFsNegotiationResponse = {
11
+ version: typeof PROJECT_FS_SENSITIVE_AUTHORITY_VERSION;
12
+ capability: ProjectFsOpaqueCapability;
13
+ nonce: string;
14
+ daemonInstallationId: string;
15
+ proof: string;
16
+ };
17
+ export declare function projectFsNegotiationProofPayload(requestId: string, challenge: ProjectFsNegotiationChallenge): string;
18
+ export declare function parseProjectFsNegotiationChallenge(value: unknown): ProjectFsNegotiationChallenge | null;
19
+ export declare function parseProjectFsNegotiationResponse(value: unknown): ProjectFsNegotiationResponse | null;
20
+ export declare const PROJECT_FS_METHODS: readonly ["project.fs.open", "project.fs.list", "project.fs.read", "project.fs.close"];
21
+ export type ProjectFsMethod = (typeof PROJECT_FS_METHODS)[number];
22
+ export declare const PROJECT_FS_LIMITS: {
23
+ readonly defaultDirectoryPageItems: 50;
24
+ readonly maxDirectoryPageItems: 200;
25
+ readonly maxReadBytes: number;
26
+ readonly maxTokenLength: 512;
27
+ readonly maxProjectRefLength: 1024;
28
+ readonly maxNameLength: 255;
29
+ };
30
+ export type ProjectFsEntryKind = 'directory' | 'file';
31
+ export type ProjectFsReadEncoding = 'binary' | 'auto';
32
+ export type ProjectFsEntry = {
33
+ handle: string;
34
+ name: string;
35
+ kind: ProjectFsEntryKind;
36
+ size?: number;
37
+ modifiedAt?: number;
38
+ };
39
+ export type ProjectFsDirectoryPage = {
40
+ directoryHandle: string;
41
+ cursor: number;
42
+ nextCursor: number | null;
43
+ entries: ProjectFsEntry[];
44
+ };
45
+ export type ProjectFsOpenParams = {
46
+ projectRef: string;
47
+ limit?: number;
48
+ };
49
+ export type ProjectFsOpenResult = {
50
+ leaseId: string;
51
+ root: {
52
+ handle: string;
53
+ name: string;
54
+ kind: 'directory';
55
+ };
56
+ page: ProjectFsDirectoryPage;
57
+ expiresAt: number;
58
+ };
59
+ export type ProjectFsListParams = {
60
+ projectRef: string;
61
+ leaseId: string;
62
+ directoryHandle: string;
63
+ cursor?: number;
64
+ limit?: number;
65
+ };
66
+ export type ProjectFsListResult = ProjectFsDirectoryPage & {
67
+ leaseId: string;
68
+ expiresAt: number;
69
+ };
70
+ export type ProjectFsReadParams = {
71
+ projectRef: string;
72
+ leaseId: string;
73
+ fileHandle: string;
74
+ offset: number;
75
+ limitBytes: number;
76
+ encoding?: ProjectFsReadEncoding;
77
+ };
78
+ export type ProjectFsReadResult = {
79
+ leaseId: string;
80
+ fileHandle: string;
81
+ offset: number;
82
+ bytesRead: number;
83
+ nextOffset: number | null;
84
+ totalSize: number;
85
+ dataBase64?: string;
86
+ text?: string;
87
+ encoding?: string;
88
+ encodingFallback?: boolean;
89
+ mimeType?: string;
90
+ expiresAt: number;
91
+ };
92
+ export type ProjectFsCloseParams = {
93
+ projectRef: string;
94
+ leaseId: string;
95
+ };
96
+ export type ProjectFsCloseResult = {
97
+ closed: true;
98
+ };
99
+ export declare const PROJECT_FS_ERROR_CODES: readonly ["capability_required", "machine_offline", "permission_denied", "project_not_found", "project_scope_changed", "handle_invalid", "handle_expired", "source_changed", "sensitive_path_denied", "unsupported_entry", "read_too_large", "rate_limited", "io_error"];
100
+ export type ProjectFsErrorCode = (typeof PROJECT_FS_ERROR_CODES)[number];
101
+ export declare const PROJECT_FS_REQUIRED_ACTIONS: readonly ["retry", "refresh_project", "upgrade_daemon", "reauthenticate", "none"];
102
+ export type ProjectFsRequiredAction = (typeof PROJECT_FS_REQUIRED_ACTIONS)[number];
103
+ /** Server-only envelope. Clients may send this key, but Server must replace it wholesale. */
104
+ export declare const PROJECT_FS_AUTHORITY_PARAM: "__projectFsAuthority";
105
+ export type ProjectFsAuthority = {
106
+ projectRef: string;
107
+ scopeId: string;
108
+ accountScope: string;
109
+ machineId: string;
110
+ projectId: string;
111
+ rootPath: string;
112
+ rootDigest: string;
113
+ authorizationEpoch: number;
114
+ daemonInstallationId: string;
115
+ generation: string;
116
+ expiresAt: number;
117
+ };
118
+ export type ProjectFsTrustedEnvelope = {
119
+ version: 1;
120
+ requestId: string;
121
+ method: ProjectFsMethod;
122
+ params: ProjectFsOpenParams | ProjectFsListParams | ProjectFsReadParams | ProjectFsCloseParams;
123
+ authority: ProjectFsAuthority;
124
+ signature: string;
125
+ };
126
+ export type ProjectFsRejection = {
127
+ code: ProjectFsErrorCode;
128
+ message?: string;
129
+ retryable: boolean;
130
+ requiredAction: ProjectFsRequiredAction;
131
+ };
132
+ export declare function isProjectFsMethod(value: unknown): value is ProjectFsMethod;
133
+ export declare function parseProjectFsDirectoryPage(value: unknown): ProjectFsDirectoryPage | null;
134
+ export declare function parseProjectFsOpenParams(value: unknown): ProjectFsOpenParams | null;
135
+ export declare function parseProjectFsOpenResult(value: unknown): ProjectFsOpenResult | null;
136
+ export declare function parseProjectFsListParams(value: unknown): ProjectFsListParams | null;
137
+ export declare function parseProjectFsListResult(value: unknown): ProjectFsListResult | null;
138
+ export declare function parseProjectFsReadParams(value: unknown): ProjectFsReadParams | null;
139
+ export declare function parseProjectFsReadResult(value: unknown): ProjectFsReadResult | null;
140
+ export declare function parseProjectFsCloseParams(value: unknown): ProjectFsCloseParams | null;
141
+ export declare function parseProjectFsCloseResult(value: unknown): ProjectFsCloseResult | null;
142
+ export declare function parseProjectFsRejection(value: unknown): ProjectFsRejection | null;
143
+ /** Future clients must require this exact capability and never probe legacy path RPCs. */
144
+ export declare function hasProjectFsOpaqueCapability(capabilities: readonly string[] | null | undefined): boolean;
145
+ export declare function parseProjectFsTrustedEnvelope(value: unknown): ProjectFsTrustedEnvelope | null;
146
+ export declare function projectFsAuthoritySigningPayload(envelope: Omit<ProjectFsTrustedEnvelope, 'signature'>): string;
@@ -0,0 +1,462 @@
1
+ // @arch docs/features/desktop-project-files-opaque-protocol.md
2
+ // @test src/__tests__/project-fs.test.ts
3
+ export const PROJECT_FS_OPAQUE_CAPABILITY = 'project_fs_opaque_v1';
4
+ export const PROJECT_FS_NEGOTIATION_METHOD = 'project.fs.negotiate';
5
+ export const PROJECT_FS_SENSITIVE_AUTHORITY_VERSION = 1;
6
+ export function projectFsNegotiationProofPayload(requestId, challenge) {
7
+ return JSON.stringify([
8
+ PROJECT_FS_NEGOTIATION_METHOD,
9
+ requestId,
10
+ challenge.version,
11
+ challenge.nonce,
12
+ challenge.daemonInstallationId,
13
+ PROJECT_FS_OPAQUE_CAPABILITY,
14
+ ]);
15
+ }
16
+ export function parseProjectFsNegotiationChallenge(value) {
17
+ const input = recordWithExactKeys(value, ['version', 'nonce', 'daemonInstallationId']);
18
+ if (!input ||
19
+ input.version !== PROJECT_FS_SENSITIVE_AUTHORITY_VERSION ||
20
+ !isOpaqueToken(input.nonce) ||
21
+ typeof input.daemonInstallationId !== 'string' ||
22
+ input.daemonInstallationId.length < 1 ||
23
+ input.daemonInstallationId.length > 191)
24
+ return null;
25
+ return {
26
+ version: PROJECT_FS_SENSITIVE_AUTHORITY_VERSION,
27
+ nonce: input.nonce,
28
+ daemonInstallationId: input.daemonInstallationId,
29
+ };
30
+ }
31
+ export function parseProjectFsNegotiationResponse(value) {
32
+ const input = recordWithExactKeys(value, [
33
+ 'version',
34
+ 'capability',
35
+ 'nonce',
36
+ 'daemonInstallationId',
37
+ 'proof',
38
+ ]);
39
+ if (!input || input.capability !== PROJECT_FS_OPAQUE_CAPABILITY)
40
+ return null;
41
+ if (input.version !== PROJECT_FS_SENSITIVE_AUTHORITY_VERSION ||
42
+ !isOpaqueToken(input.nonce) ||
43
+ typeof input.daemonInstallationId !== 'string' ||
44
+ input.daemonInstallationId.length < 1 ||
45
+ input.daemonInstallationId.length > 191 ||
46
+ !isOpaqueToken(input.proof))
47
+ return null;
48
+ return {
49
+ version: PROJECT_FS_SENSITIVE_AUTHORITY_VERSION,
50
+ capability: PROJECT_FS_OPAQUE_CAPABILITY,
51
+ nonce: input.nonce,
52
+ daemonInstallationId: input.daemonInstallationId,
53
+ proof: input.proof,
54
+ };
55
+ }
56
+ export const PROJECT_FS_METHODS = [
57
+ 'project.fs.open',
58
+ 'project.fs.list',
59
+ 'project.fs.read',
60
+ 'project.fs.close',
61
+ ];
62
+ export const PROJECT_FS_LIMITS = {
63
+ defaultDirectoryPageItems: 50,
64
+ maxDirectoryPageItems: 200,
65
+ maxReadBytes: 256 * 1024,
66
+ maxTokenLength: 512,
67
+ maxProjectRefLength: 1024,
68
+ maxNameLength: 255,
69
+ };
70
+ export const PROJECT_FS_ERROR_CODES = [
71
+ 'capability_required',
72
+ 'machine_offline',
73
+ 'permission_denied',
74
+ 'project_not_found',
75
+ 'project_scope_changed',
76
+ 'handle_invalid',
77
+ 'handle_expired',
78
+ 'source_changed',
79
+ 'sensitive_path_denied',
80
+ 'unsupported_entry',
81
+ 'read_too_large',
82
+ 'rate_limited',
83
+ 'io_error',
84
+ ];
85
+ export const PROJECT_FS_REQUIRED_ACTIONS = [
86
+ 'retry',
87
+ 'refresh_project',
88
+ 'upgrade_daemon',
89
+ 'reauthenticate',
90
+ 'none',
91
+ ];
92
+ /** Server-only envelope. Clients may send this key, but Server must replace it wholesale. */
93
+ export const PROJECT_FS_AUTHORITY_PARAM = '__projectFsAuthority';
94
+ export function isProjectFsMethod(value) {
95
+ return typeof value === 'string' && PROJECT_FS_METHODS.includes(value);
96
+ }
97
+ const OPAQUE_TOKEN_PATTERN = /^[A-Za-z0-9_-]+$/;
98
+ const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
99
+ function recordWithExactKeys(value, allowedKeys) {
100
+ if (!value || typeof value !== 'object' || Array.isArray(value))
101
+ return null;
102
+ const record = value;
103
+ for (const key of Object.keys(record)) {
104
+ if (!allowedKeys.includes(key))
105
+ return null;
106
+ }
107
+ return record;
108
+ }
109
+ function isSafeIntegerBetween(value, min, max) {
110
+ return Number.isSafeInteger(value) && Number(value) >= min && Number(value) <= max;
111
+ }
112
+ function isOpaqueToken(value) {
113
+ return (typeof value === 'string' &&
114
+ value.length >= 32 &&
115
+ value.length <= PROJECT_FS_LIMITS.maxTokenLength &&
116
+ OPAQUE_TOKEN_PATTERN.test(value));
117
+ }
118
+ function isProjectRef(value) {
119
+ return (typeof value === 'string' &&
120
+ value.length >= 32 &&
121
+ value.length <= PROJECT_FS_LIMITS.maxProjectRefLength &&
122
+ OPAQUE_TOKEN_PATTERN.test(value));
123
+ }
124
+ function isDisplayName(value) {
125
+ return (typeof value === 'string' &&
126
+ value.length > 0 &&
127
+ value.length <= PROJECT_FS_LIMITS.maxNameLength &&
128
+ !/[\u0000-\u001f\u007f\\/]/.test(value) &&
129
+ value !== '.' &&
130
+ value !== '..');
131
+ }
132
+ function optionalLimit(value) {
133
+ return (value === undefined || isSafeIntegerBetween(value, 1, PROJECT_FS_LIMITS.maxDirectoryPageItems));
134
+ }
135
+ function parseEntry(value) {
136
+ const entry = recordWithExactKeys(value, ['handle', 'name', 'kind', 'size', 'modifiedAt']);
137
+ if (!entry || !isOpaqueToken(entry.handle) || !isDisplayName(entry.name))
138
+ return null;
139
+ if (entry.kind !== 'directory' && entry.kind !== 'file')
140
+ return null;
141
+ if (entry.size !== undefined && !isSafeIntegerBetween(entry.size, 0, Number.MAX_SAFE_INTEGER)) {
142
+ return null;
143
+ }
144
+ if (entry.kind === 'directory' && entry.size !== undefined)
145
+ return null;
146
+ if (entry.modifiedAt !== undefined &&
147
+ (!Number.isFinite(entry.modifiedAt) || Number(entry.modifiedAt) < 0)) {
148
+ return null;
149
+ }
150
+ return {
151
+ handle: entry.handle,
152
+ name: entry.name,
153
+ kind: entry.kind,
154
+ ...(entry.size === undefined ? {} : { size: Number(entry.size) }),
155
+ ...(entry.modifiedAt === undefined ? {} : { modifiedAt: Number(entry.modifiedAt) }),
156
+ };
157
+ }
158
+ export function parseProjectFsDirectoryPage(value) {
159
+ const page = recordWithExactKeys(value, ['directoryHandle', 'cursor', 'nextCursor', 'entries']);
160
+ if (!page || !isOpaqueToken(page.directoryHandle))
161
+ return null;
162
+ if (!isSafeIntegerBetween(page.cursor, 0, Number.MAX_SAFE_INTEGER))
163
+ return null;
164
+ if (page.nextCursor !== null &&
165
+ !isSafeIntegerBetween(page.nextCursor, 1, Number.MAX_SAFE_INTEGER))
166
+ return null;
167
+ if (!Array.isArray(page.entries) ||
168
+ page.entries.length > PROJECT_FS_LIMITS.maxDirectoryPageItems) {
169
+ return null;
170
+ }
171
+ const entries = [];
172
+ const handles = new Set();
173
+ for (const rawEntry of page.entries) {
174
+ const entry = parseEntry(rawEntry);
175
+ if (!entry || handles.has(entry.handle))
176
+ return null;
177
+ handles.add(entry.handle);
178
+ entries.push(entry);
179
+ }
180
+ const expectedNextCursor = Number(page.cursor) + entries.length;
181
+ if (page.nextCursor !== null && page.nextCursor !== expectedNextCursor)
182
+ return null;
183
+ if (page.nextCursor === null && entries.length === 0 && Number(page.cursor) !== 0)
184
+ return null;
185
+ return {
186
+ directoryHandle: page.directoryHandle,
187
+ cursor: Number(page.cursor),
188
+ nextCursor: page.nextCursor === null ? null : Number(page.nextCursor),
189
+ entries,
190
+ };
191
+ }
192
+ export function parseProjectFsOpenParams(value) {
193
+ const params = recordWithExactKeys(value, ['projectRef', 'limit']);
194
+ if (!params || !isProjectRef(params.projectRef) || !optionalLimit(params.limit))
195
+ return null;
196
+ return {
197
+ projectRef: params.projectRef,
198
+ ...(params.limit === undefined ? {} : { limit: Number(params.limit) }),
199
+ };
200
+ }
201
+ export function parseProjectFsOpenResult(value) {
202
+ const result = recordWithExactKeys(value, ['leaseId', 'root', 'page', 'expiresAt']);
203
+ if (!result ||
204
+ !isOpaqueToken(result.leaseId) ||
205
+ !isSafeIntegerBetween(result.expiresAt, 1, Number.MAX_SAFE_INTEGER)) {
206
+ return null;
207
+ }
208
+ const root = recordWithExactKeys(result.root, ['handle', 'name', 'kind']);
209
+ if (!root ||
210
+ !isOpaqueToken(root.handle) ||
211
+ !isDisplayName(root.name) ||
212
+ root.kind !== 'directory') {
213
+ return null;
214
+ }
215
+ const page = parseProjectFsDirectoryPage(result.page);
216
+ if (!page || page.directoryHandle !== root.handle)
217
+ return null;
218
+ return {
219
+ leaseId: result.leaseId,
220
+ root: { handle: root.handle, name: root.name, kind: 'directory' },
221
+ page,
222
+ expiresAt: Number(result.expiresAt),
223
+ };
224
+ }
225
+ export function parseProjectFsListParams(value) {
226
+ const params = recordWithExactKeys(value, [
227
+ 'projectRef',
228
+ 'leaseId',
229
+ 'directoryHandle',
230
+ 'cursor',
231
+ 'limit',
232
+ ]);
233
+ if (!params ||
234
+ !isProjectRef(params.projectRef) ||
235
+ !isOpaqueToken(params.leaseId) ||
236
+ !isOpaqueToken(params.directoryHandle) ||
237
+ !optionalLimit(params.limit) ||
238
+ (params.cursor !== undefined &&
239
+ !isSafeIntegerBetween(params.cursor, 0, Number.MAX_SAFE_INTEGER))) {
240
+ return null;
241
+ }
242
+ return {
243
+ projectRef: params.projectRef,
244
+ leaseId: params.leaseId,
245
+ directoryHandle: params.directoryHandle,
246
+ ...(params.cursor === undefined ? {} : { cursor: Number(params.cursor) }),
247
+ ...(params.limit === undefined ? {} : { limit: Number(params.limit) }),
248
+ };
249
+ }
250
+ export function parseProjectFsListResult(value) {
251
+ const result = recordWithExactKeys(value, [
252
+ 'leaseId',
253
+ 'directoryHandle',
254
+ 'cursor',
255
+ 'nextCursor',
256
+ 'entries',
257
+ 'expiresAt',
258
+ ]);
259
+ if (!result ||
260
+ !isOpaqueToken(result.leaseId) ||
261
+ !isSafeIntegerBetween(result.expiresAt, 1, Number.MAX_SAFE_INTEGER))
262
+ return null;
263
+ const page = parseProjectFsDirectoryPage({
264
+ directoryHandle: result.directoryHandle,
265
+ cursor: result.cursor,
266
+ nextCursor: result.nextCursor,
267
+ entries: result.entries,
268
+ });
269
+ if (!page)
270
+ return null;
271
+ return { ...page, leaseId: result.leaseId, expiresAt: Number(result.expiresAt) };
272
+ }
273
+ export function parseProjectFsReadParams(value) {
274
+ const params = recordWithExactKeys(value, [
275
+ 'projectRef',
276
+ 'leaseId',
277
+ 'fileHandle',
278
+ 'offset',
279
+ 'limitBytes',
280
+ 'encoding',
281
+ ]);
282
+ if (!params ||
283
+ !isProjectRef(params.projectRef) ||
284
+ !isOpaqueToken(params.leaseId) ||
285
+ !isOpaqueToken(params.fileHandle) ||
286
+ !isSafeIntegerBetween(params.offset, 0, Number.MAX_SAFE_INTEGER) ||
287
+ !isSafeIntegerBetween(params.limitBytes, 1, PROJECT_FS_LIMITS.maxReadBytes) ||
288
+ (params.encoding !== undefined && params.encoding !== 'binary' && params.encoding !== 'auto')) {
289
+ return null;
290
+ }
291
+ return {
292
+ projectRef: params.projectRef,
293
+ leaseId: params.leaseId,
294
+ fileHandle: params.fileHandle,
295
+ offset: Number(params.offset),
296
+ limitBytes: Number(params.limitBytes),
297
+ ...(params.encoding === undefined ? {} : { encoding: params.encoding }),
298
+ };
299
+ }
300
+ function isBase64Page(value) {
301
+ return (typeof value === 'string' &&
302
+ value.length <= Math.ceil(PROJECT_FS_LIMITS.maxReadBytes / 3) * 4 &&
303
+ BASE64_PATTERN.test(value));
304
+ }
305
+ function base64ByteLength(value) {
306
+ if (!value)
307
+ return 0;
308
+ const padding = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;
309
+ return (value.length / 4) * 3 - padding;
310
+ }
311
+ function isOptionalMetadataString(value) {
312
+ return (value === undefined ||
313
+ (typeof value === 'string' &&
314
+ value.length > 0 &&
315
+ value.length <= 255 &&
316
+ !/[\u0000-\u001f\u007f]/.test(value)));
317
+ }
318
+ export function parseProjectFsReadResult(value) {
319
+ const result = recordWithExactKeys(value, [
320
+ 'leaseId',
321
+ 'fileHandle',
322
+ 'offset',
323
+ 'bytesRead',
324
+ 'nextOffset',
325
+ 'totalSize',
326
+ 'dataBase64',
327
+ 'text',
328
+ 'encoding',
329
+ 'encodingFallback',
330
+ 'mimeType',
331
+ 'expiresAt',
332
+ ]);
333
+ if (!result ||
334
+ !isOpaqueToken(result.leaseId) ||
335
+ !isOpaqueToken(result.fileHandle) ||
336
+ !isSafeIntegerBetween(result.offset, 0, Number.MAX_SAFE_INTEGER) ||
337
+ !isSafeIntegerBetween(result.bytesRead, 0, PROJECT_FS_LIMITS.maxReadBytes) ||
338
+ !isSafeIntegerBetween(result.totalSize, 0, Number.MAX_SAFE_INTEGER) ||
339
+ !isSafeIntegerBetween(result.expiresAt, 1, Number.MAX_SAFE_INTEGER))
340
+ return null;
341
+ const expectedNextOffset = Number(result.offset) + Number(result.bytesRead);
342
+ if (expectedNextOffset > Number(result.totalSize))
343
+ return null;
344
+ if (result.nextOffset !== null && result.nextOffset !== expectedNextOffset)
345
+ return null;
346
+ if (result.nextOffset === null && expectedNextOffset !== Number(result.totalSize))
347
+ return null;
348
+ const hasBinary = result.dataBase64 !== undefined;
349
+ const hasText = result.text !== undefined;
350
+ if (hasBinary === hasText)
351
+ return null;
352
+ if (hasBinary && !isBase64Page(result.dataBase64))
353
+ return null;
354
+ if (hasBinary && base64ByteLength(result.dataBase64) !== Number(result.bytesRead)) {
355
+ return null;
356
+ }
357
+ if (hasText &&
358
+ (typeof result.text !== 'string' || result.text.length > PROJECT_FS_LIMITS.maxReadBytes)) {
359
+ return null;
360
+ }
361
+ if (!isOptionalMetadataString(result.encoding) || !isOptionalMetadataString(result.mimeType)) {
362
+ return null;
363
+ }
364
+ if (result.encodingFallback !== undefined && typeof result.encodingFallback !== 'boolean') {
365
+ return null;
366
+ }
367
+ return {
368
+ leaseId: result.leaseId,
369
+ fileHandle: result.fileHandle,
370
+ offset: Number(result.offset),
371
+ bytesRead: Number(result.bytesRead),
372
+ nextOffset: result.nextOffset === null ? null : Number(result.nextOffset),
373
+ totalSize: Number(result.totalSize),
374
+ ...(hasBinary ? { dataBase64: result.dataBase64 } : { text: result.text }),
375
+ ...(result.encoding === undefined ? {} : { encoding: result.encoding }),
376
+ ...(result.encodingFallback === undefined ? {} : { encodingFallback: result.encodingFallback }),
377
+ ...(result.mimeType === undefined ? {} : { mimeType: result.mimeType }),
378
+ expiresAt: Number(result.expiresAt),
379
+ };
380
+ }
381
+ export function parseProjectFsCloseParams(value) {
382
+ const params = recordWithExactKeys(value, ['projectRef', 'leaseId']);
383
+ if (!params || !isProjectRef(params.projectRef) || !isOpaqueToken(params.leaseId))
384
+ return null;
385
+ return { projectRef: params.projectRef, leaseId: params.leaseId };
386
+ }
387
+ export function parseProjectFsCloseResult(value) {
388
+ const result = recordWithExactKeys(value, ['closed']);
389
+ return result?.closed === true ? { closed: true } : null;
390
+ }
391
+ export function parseProjectFsRejection(value) {
392
+ const rejection = recordWithExactKeys(value, ['code', 'message', 'retryable', 'requiredAction']);
393
+ if (!rejection ||
394
+ !PROJECT_FS_ERROR_CODES.includes(rejection.code) ||
395
+ !PROJECT_FS_REQUIRED_ACTIONS.includes(rejection.requiredAction) ||
396
+ typeof rejection.retryable !== 'boolean' ||
397
+ (rejection.message !== undefined &&
398
+ (typeof rejection.message !== 'string' ||
399
+ rejection.message.length > 512 ||
400
+ /[\u0000-\u001f\u007f]/.test(rejection.message) ||
401
+ /(?:^|\s)(?:\/[A-Za-z0-9._-]|[A-Za-z]:[\\/]|\\\\)/.test(rejection.message))))
402
+ return null;
403
+ return {
404
+ code: rejection.code,
405
+ ...(rejection.message === undefined ? {} : { message: rejection.message }),
406
+ retryable: rejection.retryable,
407
+ requiredAction: rejection.requiredAction,
408
+ };
409
+ }
410
+ /** Future clients must require this exact capability and never probe legacy path RPCs. */
411
+ export function hasProjectFsOpaqueCapability(capabilities) {
412
+ return capabilities?.includes(PROJECT_FS_OPAQUE_CAPABILITY) === true;
413
+ }
414
+ export function parseProjectFsTrustedEnvelope(value) {
415
+ const envelope = recordWithExactKeys(value, [
416
+ 'version', 'requestId', 'method', 'params', 'authority', 'signature',
417
+ ]);
418
+ if (!envelope || envelope.version !== 1 || typeof envelope.requestId !== 'string' ||
419
+ !envelope.requestId || envelope.requestId.length > 191 || !isProjectFsMethod(envelope.method) ||
420
+ typeof envelope.signature !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(envelope.signature))
421
+ return null;
422
+ const params = envelope.method === 'project.fs.open'
423
+ ? parseProjectFsOpenParams(envelope.params)
424
+ : envelope.method === 'project.fs.list'
425
+ ? parseProjectFsListParams(envelope.params)
426
+ : envelope.method === 'project.fs.read'
427
+ ? parseProjectFsReadParams(envelope.params)
428
+ : parseProjectFsCloseParams(envelope.params);
429
+ const authority = recordWithExactKeys(envelope.authority, [
430
+ 'projectRef', 'scopeId', 'accountScope', 'machineId', 'projectId', 'rootPath', 'rootDigest',
431
+ 'authorizationEpoch', 'daemonInstallationId', 'generation', 'expiresAt',
432
+ ]);
433
+ if (!params || !authority || authority.projectRef !== params.projectRef ||
434
+ !isProjectRef(authority.projectRef) || !isOpaqueToken(authority.scopeId) ||
435
+ !isOpaqueToken(authority.accountScope) || typeof authority.machineId !== 'string' ||
436
+ !authority.machineId || authority.machineId.length > 191 || typeof authority.projectId !== 'string' ||
437
+ !authority.projectId || authority.projectId.length > 64 || typeof authority.rootPath !== 'string' ||
438
+ !authority.rootPath || authority.rootPath.length > 4096 || !isOpaqueToken(authority.rootDigest) ||
439
+ !isSafeIntegerBetween(authority.authorizationEpoch, 0, Number.MAX_SAFE_INTEGER) ||
440
+ typeof authority.daemonInstallationId !== 'string' || !authority.daemonInstallationId ||
441
+ authority.daemonInstallationId.length > 191 || typeof authority.generation !== 'string' ||
442
+ !isOpaqueToken(authority.generation) ||
443
+ !isSafeIntegerBetween(authority.expiresAt, 1, Number.MAX_SAFE_INTEGER))
444
+ return null;
445
+ return {
446
+ version: 1,
447
+ requestId: envelope.requestId,
448
+ method: envelope.method,
449
+ params,
450
+ authority: authority,
451
+ signature: envelope.signature,
452
+ };
453
+ }
454
+ export function projectFsAuthoritySigningPayload(envelope) {
455
+ const authority = envelope.authority;
456
+ return JSON.stringify([
457
+ envelope.version, envelope.requestId, envelope.method, envelope.params,
458
+ authority.projectRef, authority.scopeId, authority.accountScope, authority.machineId,
459
+ authority.projectId, authority.rootPath, authority.rootDigest, authority.authorizationEpoch,
460
+ authority.daemonInstallationId, authority.generation, authority.expiresAt,
461
+ ]);
462
+ }
@@ -11,4 +11,6 @@ export type NianProject = {
11
11
  lastActivityAt: string;
12
12
  createdAt: string;
13
13
  updatedAt: string;
14
+ /** Server-minted opaque reference for project-scoped file RPCs. */
15
+ fileBrowserRef?: string;
14
16
  };
@@ -1 +1,3 @@
1
+ // @arch docs/features/desktop-project-files-opaque-protocol.md
2
+ // @test src/__tests__/project-fs.test.ts
1
3
  export {};
@@ -1,5 +1,6 @@
1
1
  import type { Envelope } from './envelope.ts';
2
2
  import type { AgentType, ManagerConfig, SessionMode } from './session.ts';
3
+ import type { ProjectFsOpaqueCapability } from './project-fs.ts';
3
4
  export declare const SESSION_SYNC_PROTOCOL_VERSION: 2;
4
5
  export type SessionSyncProtocolVersion = typeof SESSION_SYNC_PROTOCOL_VERSION;
5
6
  /**
@@ -14,6 +15,7 @@ export declare const SESSION_SYNC_RELEASE_GATE: {
14
15
  readonly ios: 51;
15
16
  readonly android: 12;
16
17
  };
18
+ readonly minDesktopVersion: "0.4.0";
17
19
  };
18
20
  export declare const SESSION_SYNC_REQUIRED_CAPABILITIES: readonly ["session_index_v1", "session_body_revision_v1", "session_history_subscription_v1", "local_canonical_v1", "session_title_authority_v1", "session_title_seed_v1", "session_tool_detail_v1", "voice_session_body_relay_v1", "native_source_tombstone_v1", "session_durable_send_v1", "daemon_installation_identity_v1", "session_user_boundary_v1", "session_terminal_outbox_v1", "session_list_preview_v1", "session_attention_v1", "session_data_epoch_v2", "session_structured_rejection_v2", "machine_personal_authority_v2"];
19
21
  /**
@@ -36,7 +38,12 @@ export declare const SESSION_HISTORY_LIMITS: {
36
38
  readonly maxSubscriptionsPerSession: 8;
37
39
  readonly maxReplicaStreamsPerDaemon: 8;
38
40
  };
39
- export type SessionSyncCapability = (typeof SESSION_SYNC_ADVERTISED_CAPABILITIES)[number];
41
+ /**
42
+ * Known capabilities accepted by the handshake projection. A capability can be
43
+ * known before a daemon is allowed to advertise it; advertisement must follow
44
+ * the implementation-specific rollout gate.
45
+ */
46
+ export type SessionSyncCapability = (typeof SESSION_SYNC_ADVERTISED_CAPABILITIES)[number] | ProjectFsOpaqueCapability;
40
47
  export type SessionSyncEventName = (typeof SESSION_SYNC_EVENT_NAMES)[number];
41
48
  export type SessionBodyStorageMode = 'machine_local';
42
49
  export type NativeCleanupPolicy = 'none' | 'auto';
@@ -9,6 +9,7 @@ export const SESSION_SYNC_DATA_EPOCH = 2;
9
9
  export const SESSION_SYNC_RELEASE_GATE = {
10
10
  minCliVersion: '0.3.65',
11
11
  minAppBuild: { ios: 51, android: 12 },
12
+ minDesktopVersion: '0.4.0',
12
13
  };
13
14
  export const SESSION_SYNC_REQUIRED_CAPABILITIES = [
14
15
  'session_index_v1',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.4.30",
3
+ "version": "0.4.32",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {