shennian 0.3.85 → 0.3.86
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.
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +28 -28
- package/dist/src/agents/adapter.js +19 -1
- package/dist/src/agents/claude-managed-permissions.js +248 -1
- package/dist/src/agents/claude.js +393 -8
- package/dist/src/agents/codex-managed-permissions.js +294 -1
- package/dist/src/agents/codex-managed-room-tools.js +249 -1
- package/dist/src/agents/codex-utils.js +200 -7
- package/dist/src/agents/codex.js +1352 -17
- package/dist/src/agents/config-status.js +226 -1
- package/dist/src/agents/cursor-managed-permissions.js +299 -2
- package/dist/src/agents/cursor.js +314 -1
- package/dist/src/agents/custom.js +418 -4
- package/dist/src/agents/external-channel-instructions.js +94 -11
- package/dist/src/agents/gemini.js +180 -1
- package/dist/src/agents/manager.d.ts +2 -0
- package/dist/src/agents/manager.js +250 -18
- package/dist/src/agents/model-registry/cache.js +37 -1
- package/dist/src/agents/model-registry/discovery.js +188 -2
- package/dist/src/agents/model-registry/parsers.js +521 -4
- package/dist/src/agents/model-registry/runner.js +30 -1
- package/dist/src/agents/model-registry/service.js +115 -1
- package/dist/src/agents/model-registry/types.js +8 -1
- package/dist/src/agents/model-registry.js +19 -1
- package/dist/src/agents/native-capabilities.js +237 -1
- package/dist/src/agents/openclaw.js +293 -2
- package/dist/src/agents/opencode-managed-permissions.js +187 -2
- package/dist/src/agents/opencode.js +298 -1
- package/dist/src/agents/platform-instructions.js +46 -17
- package/dist/src/agents/runtime-compatibility.js +44 -1
- package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
- package/dist/src/agents/workbuddy.js +444 -3
- package/dist/src/commands/room.js +2 -2
- package/dist/src/fs/boundary.js +185 -1
- package/dist/src/fs/handler.js +130 -1
- package/dist/src/fs/security.js +32 -1
- package/dist/src/fs/text-decoder.js +110 -1
- package/dist/src/index.js +587 -3
- package/dist/src/manager/prompt.js +34 -29
- package/dist/src/manager/registry.js +317 -1
- package/dist/src/manager/retired-external-channel-compat.js +40 -1
- package/dist/src/manager/runtime.d.ts +4 -0
- package/dist/src/manager/runtime.js +1514 -23
- package/dist/src/native-fusion/codex-parser.js +660 -4
- package/dist/src/native-fusion/codex-user-input.js +102 -5
- package/dist/src/native-fusion/config.js +5 -1
- package/dist/src/native-fusion/file-watcher.js +191 -1
- package/dist/src/native-fusion/local-repository.js +569 -2
- package/dist/src/native-fusion/opencode-parser.js +130 -3
- package/dist/src/native-fusion/parsers.js +140 -3
- package/dist/src/native-fusion/service.js +739 -2
- package/dist/src/native-fusion/session-header.js +101 -2
- package/dist/src/native-fusion/state.js +104 -2
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/personal-sync/machine-authority.js +34 -1
- package/dist/src/room/activation-audit.js +45 -2
- package/dist/src/room/activation-policy.js +30 -1
- package/dist/src/room/activation-service.d.ts +1 -0
- package/dist/src/room/activation-service.js +408 -8
- package/dist/src/room/invite-bootstrap.js +156 -1
- package/dist/src/room/managed-context.d.ts +1 -1
- package/dist/src/room/managed-context.js +1 -1
- package/dist/src/room/managed-prompt-composer.d.ts +1 -1
- package/dist/src/room/managed-prompt-composer.js +6 -5
- package/dist/src/room/managed-room-binding.js +167 -1
- package/dist/src/room/managed-room-ipc.d.ts +2 -2
- package/dist/src/room/managed-room-ipc.js +2 -2
- package/dist/src/room/mirror-sync-service.js +183 -1
- package/dist/src/room/room-action-client.d.ts +11 -0
- package/dist/src/room/room-action-client.js +1 -1
- package/dist/src/room/room-binding-client.d.ts +1 -0
- package/dist/src/room/room-binding-client.js +151 -1
- package/dist/src/security/managed-agent-policy.d.ts +3 -2
- package/dist/src/security/managed-agent-policy.js +1 -1
- package/dist/src/session/archive-zip.js +220 -1
- package/dist/src/session/deletion-sync-state.js +136 -2
- package/dist/src/session/handlers/agent-capabilities.js +14 -1
- package/dist/src/session/handlers/agent-config.js +150 -1
- package/dist/src/session/handlers/agents.js +55 -1
- package/dist/src/session/handlers/chat.d.ts +1 -1
- package/dist/src/session/handlers/chat.js +1030 -2
- package/dist/src/session/handlers/control.js +61 -1
- package/dist/src/session/handlers/fs.js +594 -1
- package/dist/src/session/handlers/message-status.js +15 -1
- package/dist/src/session/handlers/remote-access.js +43 -1
- package/dist/src/session/handlers/room-managed.js +101 -1
- package/dist/src/session/handlers/session-refresh.js +35 -1
- package/dist/src/session/handlers/title.js +79 -1
- package/dist/src/session/handlers/tool-detail.js +149 -1
- package/dist/src/session/handlers/voice-window.js +62 -1
- package/dist/src/session/history-subscriptions.js +470 -1
- package/dist/src/session/manager.js +658 -1
- package/dist/src/session/native-cleanup-outbox.js +287 -2
- package/dist/src/session/native-session-lifecycle.js +327 -6
- package/dist/src/session/queue.d.ts +5 -0
- package/dist/src/session/queue.js +8 -8
- package/dist/src/upgrade/scheduler.js +126 -1
- package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1,151 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/features/network-rooms/multi-session-client-and-room-panel.md#32-每次调用都必须带可信调用上下文
|
|
2
|
+
// @test src/__tests__/room-binding-client.test.ts
|
|
3
|
+
import { HttpRoomActionClient } from './room-action-client.js';
|
|
4
|
+
export class RoomBindingHttpError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = 'RoomBindingHttpError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class HttpRoomBindingClient {
|
|
13
|
+
fetchImpl;
|
|
14
|
+
baseUrl;
|
|
15
|
+
actionClient;
|
|
16
|
+
constructor(serverUrl, fetchImpl = fetch) {
|
|
17
|
+
this.fetchImpl = fetchImpl;
|
|
18
|
+
this.baseUrl = serverUrl.replace(/\/+$/, '');
|
|
19
|
+
this.actionClient = new HttpRoomActionClient(serverUrl, fetchImpl);
|
|
20
|
+
}
|
|
21
|
+
status = (target) => this.actionClient.status(target);
|
|
22
|
+
getAttachmentCapability = (target) => this.actionClient.getAttachmentCapability(target);
|
|
23
|
+
initializeAttachment = (target) => this.actionClient.initializeAttachment(target);
|
|
24
|
+
completeAttachment = (target) => this.actionClient.completeAttachment(target);
|
|
25
|
+
createRoom = (input) => this.actionClient.createRoom(input);
|
|
26
|
+
createOperatorRoom = (input) => this.actionClient.createOperatorRoom(input);
|
|
27
|
+
resolveNativeSession = (input) => this.actionClient.resolveNativeSession(input);
|
|
28
|
+
joinRoom = (input) => this.actionClient.joinRoom(input);
|
|
29
|
+
read = (target) => this.actionClient.read(target);
|
|
30
|
+
send = (target) => this.actionClient.send(target);
|
|
31
|
+
mode = (target) => this.actionClient.mode(target);
|
|
32
|
+
pause = (target) => this.actionClient.pause(target);
|
|
33
|
+
async create(input) {
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = await this.fetchImpl(`${this.baseUrl}/api/rooms/${encodeURIComponent(input.roomId)}/bindings`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
authorization: `Bearer ${input.machineToken}`,
|
|
40
|
+
'content-type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
bindingIntentId: input.bindingIntentId,
|
|
44
|
+
agentIdentityId: input.agentIdentityId,
|
|
45
|
+
nianSessionId: input.nianSessionId,
|
|
46
|
+
runtimeAdapter: input.runtimeAdapter,
|
|
47
|
+
sessionRegistryKey: input.sessionRegistryKey,
|
|
48
|
+
activationCapability: input.activationCapability,
|
|
49
|
+
activationMode: input.activationMode,
|
|
50
|
+
}),
|
|
51
|
+
signal: AbortSignal.timeout(10_000),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new RoomBindingHttpError('room_binding_server_unavailable', 'Shennian Room service is unavailable.');
|
|
56
|
+
}
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
let code = `room_binding_http_${response.status}`;
|
|
59
|
+
try {
|
|
60
|
+
const body = (await response.json());
|
|
61
|
+
if (typeof body.error === 'string' && /^[a-z0-9_-]{1,80}$/.test(body.error))
|
|
62
|
+
code = body.error;
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
throw new RoomBindingHttpError(code, 'Shennian Room binding request failed.');
|
|
66
|
+
}
|
|
67
|
+
let body;
|
|
68
|
+
try {
|
|
69
|
+
body = await response.json();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new RoomBindingHttpError('invalid_room_binding_response', 'Invalid Room binding response.');
|
|
73
|
+
}
|
|
74
|
+
return validateResponse(body, input);
|
|
75
|
+
}
|
|
76
|
+
async restart(input) {
|
|
77
|
+
let response;
|
|
78
|
+
try {
|
|
79
|
+
response = await this.fetchImpl(`${this.baseUrl}/api/rooms/${encodeURIComponent(input.roomId)}/bindings/${encodeURIComponent(input.bindingId)}/session`, {
|
|
80
|
+
method: 'PATCH',
|
|
81
|
+
headers: {
|
|
82
|
+
authorization: `Bearer ${input.machineToken}`,
|
|
83
|
+
'content-type': 'application/json',
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
agentIdentityId: input.agentIdentityId,
|
|
87
|
+
previousNianSessionId: input.previousNianSessionId,
|
|
88
|
+
nianSessionId: input.nianSessionId,
|
|
89
|
+
runtimeAdapter: input.runtimeAdapter,
|
|
90
|
+
sessionRegistryKey: input.sessionRegistryKey,
|
|
91
|
+
}),
|
|
92
|
+
signal: AbortSignal.timeout(10_000),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new RoomBindingHttpError('room_binding_server_unavailable', 'Shennian Room service is unavailable.');
|
|
97
|
+
}
|
|
98
|
+
const body = await response.json().catch(() => null);
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const code = body && typeof body.error === 'string' && /^[a-z0-9_-]{1,80}$/.test(body.error)
|
|
101
|
+
? body.error
|
|
102
|
+
: `room_binding_http_${response.status}`;
|
|
103
|
+
throw new RoomBindingHttpError(code, 'Shennian Room restart request failed.');
|
|
104
|
+
}
|
|
105
|
+
const result = body;
|
|
106
|
+
if (!result?.binding ||
|
|
107
|
+
result.binding.id !== input.bindingId ||
|
|
108
|
+
result.binding.roomId !== input.roomId ||
|
|
109
|
+
result.binding.agentIdentityId !== input.agentIdentityId ||
|
|
110
|
+
result.binding.nianSessionId !== input.nianSessionId ||
|
|
111
|
+
result.binding.runtimeAdapter !== input.runtimeAdapter ||
|
|
112
|
+
typeof result.binding.agentMembershipId !== 'string' ||
|
|
113
|
+
typeof result.resumeSequence !== 'string' ||
|
|
114
|
+
!/^(0|[1-9]\d{0,19})$/.test(result.resumeSequence) ||
|
|
115
|
+
typeof result.idempotent !== 'boolean') {
|
|
116
|
+
throw new RoomBindingHttpError('invalid_room_binding_response', 'Invalid Room restart response.');
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
binding: result.binding,
|
|
120
|
+
resumeSequence: result.resumeSequence,
|
|
121
|
+
idempotent: result.idempotent,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function validateResponse(value, input) {
|
|
126
|
+
if (!value || typeof value !== 'object') {
|
|
127
|
+
throw new RoomBindingHttpError('invalid_room_binding_response', 'Invalid Room binding response.');
|
|
128
|
+
}
|
|
129
|
+
const object = value;
|
|
130
|
+
const binding = object.binding;
|
|
131
|
+
if (!binding ||
|
|
132
|
+
typeof binding.id !== 'string' ||
|
|
133
|
+
typeof binding.agentMembershipId !== 'string' ||
|
|
134
|
+
binding.roomId !== input.roomId ||
|
|
135
|
+
binding.agentIdentityId !== input.agentIdentityId ||
|
|
136
|
+
binding.nianSessionId !== input.nianSessionId ||
|
|
137
|
+
binding.runtimeAdapter !== input.runtimeAdapter ||
|
|
138
|
+
binding.activationCapability !== input.activationCapability ||
|
|
139
|
+
!matchesRequestedActivationMode(binding.activationMode, input) ||
|
|
140
|
+
!['pending_approval', 'active', 'paused', 'detached'].includes(binding.status ?? '') ||
|
|
141
|
+
typeof object.idempotent !== 'boolean') {
|
|
142
|
+
throw new RoomBindingHttpError('invalid_room_binding_response', 'Room binding response does not match the request.');
|
|
143
|
+
}
|
|
144
|
+
return { binding: binding, idempotent: object.idempotent };
|
|
145
|
+
}
|
|
146
|
+
function matchesRequestedActivationMode(responseMode, input) {
|
|
147
|
+
if (input.activationCapability === 'managed' && input.activationMode === 'auto_follow') {
|
|
148
|
+
return responseMode === 'auto_follow' || responseMode === 'mention_only';
|
|
149
|
+
}
|
|
150
|
+
return responseMode === input.activationMode;
|
|
151
|
+
}
|
|
@@ -49,7 +49,8 @@ export type ManagedAgentPolicyCompileInput = ManagedAgentRuntimeResolutionInput
|
|
|
49
49
|
};
|
|
50
50
|
/**
|
|
51
51
|
* Turns untrusted policy input into one immutable per-turn runtime snapshot.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Invalid input and unconfirmed host-wide access fail. Native enforcement gaps
|
|
53
|
+
* remain explicit in the immutable snapshot so the prompt can preserve the
|
|
54
|
+
* user's authorization ceiling while each adapter applies what it supports.
|
|
54
55
|
*/
|
|
55
56
|
export declare function compileManagedAgentPolicy(input: ManagedAgentPolicyCompileInput): Promise<ManagedAgentPolicyCompileResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import c from"node:fs/promises";import i from"node:path";import{parseManagedAgentPermissionPolicy as v,resolveManagedAgentEffectivePermissionPolicy as y}from"@shennian/wire";import{getManagedAgentBaseEnvKeys as _,getManagedAgentProviderEnvKeys as f,MANAGED_AGENT_SESSION_ENV_KEYS as u}from"../agent-env.js";import{collectPolicyEnforcementGaps as k,resolveManagedAgentRuntimeProtection as b,resolveManagedAgentSecurityProfile as w}from"./managed-agent-runtime.js";import{resolveProtectedShennianDirectories as P}from"./managed-agent-credential-paths.js";function W(e){return{...e.enforcement,effectivePolicy:{version:e.adapterPolicy.version,fileAccess:e.adapterPolicy.fileAccess,externalCapabilities:{...e.adapterPolicy.externalCapabilities}}}}async function R(e){const r=v(e.policy);if(!r)return{ok:!1,errorCode:"managed_agent_policy_invalid"};if(r.fileAccess==="host-full"&&!e.hostFullAccessConfirmed)return{ok:!1,errorCode:"managed_agent_host_full_confirmation_required"};const t=await C(e.workDir,e.resolveWorkDir);if(!t.ok)return t;if(await E(t.workDir))return{ok:!1,errorCode:"managed_agent_workdir_invalid"};const n=A(e),o=b(n),a=w(n),s=k(r,a),l=g([...o.level==="hard"?[]:["runtime.hardIsolation"],...o.unsupportedCapabilities,...s.unavailableCapabilities,...s.overgrantedCapabilities]),p=y(r,o),m={level:o.level,effectivePolicy:p,unsupportedCapabilities:l,runtimeVersion:o.runtimeVersion,platform:o.platform,...o.executableFingerprint?{executableFingerprint:o.executableFingerprint}:{}};return{ok:!0,value:{requestedPolicy:x(r),enforcement:m,adapterPolicy:{version:r.version,workDir:t.workDir,fileAccess:r.fileAccess,externalCapabilities:{...r.externalCapabilities},denySensitiveFiles:!0,failClosed:!0},environmentPolicy:{inheritDaemonEnv:!1,loadLoginShell:!1,providerEnvKeys:[...f(e.agentType)],sessionEnvKeys:[...u],shellSubprocess:{inherit:"none",allowedEnvKeys:g([..._(e.platform),...u]),deniedEnvKeys:[...f(e.agentType)]}}}}}function A(e){return{agentType:e.agentType,runtimeVersion:e.runtimeVersion,platform:e.platform,isWsl:e.isWsl,executableFingerprint:e.executableFingerprint,requireExecutableFingerprint:e.requireExecutableFingerprint,securityDeclaration:e.securityDeclaration}}async function C(e,r){const t=e.trim();if(!t||!i.isAbsolute(t))return{ok:!1,errorCode:"managed_agent_workdir_invalid"};try{const n=await(r??h)(t);return!n||!i.isAbsolute(n)?{ok:!1,errorCode:"managed_agent_workdir_invalid"}:{ok:!0,workDir:i.normalize(n)}}catch{return{ok:!1,errorCode:"managed_agent_workdir_unavailable"}}}async function h(e){const r=await c.realpath(e);if(!(await c.stat(r)).isDirectory())throw new Error("managed_agent_workdir_not_directory");return r}async function E(e){return P().some(t=>D(e,t,process.platform))}function D(e,r,t){const n=s=>{const l=i.resolve(s);return t==="win32"?l.toLowerCase():l},o=n(e),a=n(r);return d(o,a)||d(a,o)}function d(e,r){const t=i.relative(e,r);return t===""||!t.startsWith("..")&&!i.isAbsolute(t)}function x(e){return{...e,externalCapabilities:{...e.externalCapabilities}}}function g(e){return[...new Set(e)]}export{R as compileManagedAgentPolicy,W as resolveManagedAgentAdapterEnforcement};
|
|
@@ -1 +1,220 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/features/file-management-enhancements.md#文件夹打包下载
|
|
2
|
+
// @test src/__tests__/archive-zip.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const DEFAULT_MAX_FILES = 5000;
|
|
6
|
+
const DEFAULT_MAX_TOTAL_SIZE = 1024 * 1024 * 1024;
|
|
7
|
+
const ZIP_VERSION_NEEDED = 20;
|
|
8
|
+
const ZIP_UTF8_FLAG = 0x0800;
|
|
9
|
+
const ZIP_STORE_METHOD = 0;
|
|
10
|
+
const CHUNK_SIZE = 1024 * 1024;
|
|
11
|
+
const crcTable = new Uint32Array(256);
|
|
12
|
+
for (let index = 0; index < 256; index += 1) {
|
|
13
|
+
let value = index;
|
|
14
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
15
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
16
|
+
}
|
|
17
|
+
crcTable[index] = value >>> 0;
|
|
18
|
+
}
|
|
19
|
+
function updateCrc32(crc, buffer) {
|
|
20
|
+
let next = crc;
|
|
21
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
22
|
+
next = crcTable[(next ^ buffer[index]) & 0xff] ^ (next >>> 8);
|
|
23
|
+
}
|
|
24
|
+
return next >>> 0;
|
|
25
|
+
}
|
|
26
|
+
function dosDateTime(mtimeMs) {
|
|
27
|
+
const date = new Date(mtimeMs || Date.now());
|
|
28
|
+
const year = Math.max(1980, Math.min(2107, date.getFullYear()));
|
|
29
|
+
return {
|
|
30
|
+
date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
|
|
31
|
+
time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function writeUInt32(buffer, value, offset) {
|
|
35
|
+
buffer.writeUInt32LE(value >>> 0, offset);
|
|
36
|
+
}
|
|
37
|
+
function writeLocalHeader(fd, entry) {
|
|
38
|
+
const name = Buffer.from(entry.name, 'utf8');
|
|
39
|
+
const { date, time } = dosDateTime(entry.mtimeMs);
|
|
40
|
+
const header = Buffer.alloc(30);
|
|
41
|
+
writeUInt32(header, 0x04034b50, 0);
|
|
42
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 4);
|
|
43
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 6);
|
|
44
|
+
header.writeUInt16LE(ZIP_STORE_METHOD, 8);
|
|
45
|
+
header.writeUInt16LE(time, 10);
|
|
46
|
+
header.writeUInt16LE(date, 12);
|
|
47
|
+
writeUInt32(header, entry.crc32, 14);
|
|
48
|
+
writeUInt32(header, entry.size, 18);
|
|
49
|
+
writeUInt32(header, entry.size, 22);
|
|
50
|
+
header.writeUInt16LE(name.length, 26);
|
|
51
|
+
header.writeUInt16LE(0, 28);
|
|
52
|
+
fs.writeSync(fd, header);
|
|
53
|
+
fs.writeSync(fd, name);
|
|
54
|
+
}
|
|
55
|
+
function writeCentralDirectoryHeader(fd, entry) {
|
|
56
|
+
const name = Buffer.from(entry.name, 'utf8');
|
|
57
|
+
const { date, time } = dosDateTime(entry.mtimeMs);
|
|
58
|
+
const header = Buffer.alloc(46);
|
|
59
|
+
writeUInt32(header, 0x02014b50, 0);
|
|
60
|
+
header.writeUInt16LE(0x031e, 4);
|
|
61
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 6);
|
|
62
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 8);
|
|
63
|
+
header.writeUInt16LE(ZIP_STORE_METHOD, 10);
|
|
64
|
+
header.writeUInt16LE(time, 12);
|
|
65
|
+
header.writeUInt16LE(date, 14);
|
|
66
|
+
writeUInt32(header, entry.crc32, 16);
|
|
67
|
+
writeUInt32(header, entry.size, 20);
|
|
68
|
+
writeUInt32(header, entry.size, 24);
|
|
69
|
+
header.writeUInt16LE(name.length, 28);
|
|
70
|
+
header.writeUInt16LE(0, 30);
|
|
71
|
+
header.writeUInt16LE(0, 32);
|
|
72
|
+
header.writeUInt16LE(0, 34);
|
|
73
|
+
header.writeUInt16LE(0, 36);
|
|
74
|
+
writeUInt32(header, entry.isDir ? 0x10 : 0, 38);
|
|
75
|
+
writeUInt32(header, entry.offset, 42);
|
|
76
|
+
fs.writeSync(fd, header);
|
|
77
|
+
fs.writeSync(fd, name);
|
|
78
|
+
}
|
|
79
|
+
function writeEndOfCentralDirectory(fd, entryCount, centralSize, centralOffset) {
|
|
80
|
+
const header = Buffer.alloc(22);
|
|
81
|
+
writeUInt32(header, 0x06054b50, 0);
|
|
82
|
+
header.writeUInt16LE(0, 4);
|
|
83
|
+
header.writeUInt16LE(0, 6);
|
|
84
|
+
header.writeUInt16LE(entryCount, 8);
|
|
85
|
+
header.writeUInt16LE(entryCount, 10);
|
|
86
|
+
writeUInt32(header, centralSize, 12);
|
|
87
|
+
writeUInt32(header, centralOffset, 16);
|
|
88
|
+
header.writeUInt16LE(0, 20);
|
|
89
|
+
fs.writeSync(fd, header);
|
|
90
|
+
}
|
|
91
|
+
function normalizeZipName(relativePath, isDir) {
|
|
92
|
+
const normalized = relativePath.split(path.sep).join('/');
|
|
93
|
+
return isDir && !normalized.endsWith('/') ? `${normalized}/` : normalized;
|
|
94
|
+
}
|
|
95
|
+
function collectEntries(sourceDir, options) {
|
|
96
|
+
const sourceStat = fs.statSync(sourceDir);
|
|
97
|
+
const rootName = normalizeZipName(path.basename(sourceDir) || 'folder', true);
|
|
98
|
+
const entries = [{
|
|
99
|
+
name: rootName,
|
|
100
|
+
sourcePath: null,
|
|
101
|
+
crc32: 0,
|
|
102
|
+
size: 0,
|
|
103
|
+
isDir: true,
|
|
104
|
+
mtimeMs: sourceStat.mtimeMs,
|
|
105
|
+
}];
|
|
106
|
+
let fileCount = 0;
|
|
107
|
+
let totalSize = 0;
|
|
108
|
+
const visit = (dirPath) => {
|
|
109
|
+
const raw = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
110
|
+
for (const dirent of raw) {
|
|
111
|
+
const fullPath = path.join(dirPath, dirent.name);
|
|
112
|
+
const relative = path.relative(sourceDir, fullPath);
|
|
113
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
114
|
+
continue;
|
|
115
|
+
const stat = fs.lstatSync(fullPath);
|
|
116
|
+
if (stat.isSymbolicLink())
|
|
117
|
+
continue;
|
|
118
|
+
if (stat.isDirectory()) {
|
|
119
|
+
entries.push({
|
|
120
|
+
name: path.posix.join(rootName, normalizeZipName(relative, true)),
|
|
121
|
+
sourcePath: null,
|
|
122
|
+
crc32: 0,
|
|
123
|
+
size: 0,
|
|
124
|
+
isDir: true,
|
|
125
|
+
mtimeMs: stat.mtimeMs,
|
|
126
|
+
});
|
|
127
|
+
visit(fullPath);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!stat.isFile())
|
|
131
|
+
continue;
|
|
132
|
+
fileCount += 1;
|
|
133
|
+
if (fileCount > options.maxFiles)
|
|
134
|
+
throw new Error(`Too many files: ${fileCount}`);
|
|
135
|
+
totalSize += stat.size;
|
|
136
|
+
if (totalSize > options.maxTotalSize) {
|
|
137
|
+
throw new Error(`Folder too large: ${totalSize} bytes`);
|
|
138
|
+
}
|
|
139
|
+
entries.push({
|
|
140
|
+
name: path.posix.join(rootName, normalizeZipName(relative, false)),
|
|
141
|
+
sourcePath: fullPath,
|
|
142
|
+
crc32: computeFileCrc32(fullPath),
|
|
143
|
+
size: stat.size,
|
|
144
|
+
isDir: false,
|
|
145
|
+
mtimeMs: stat.mtimeMs,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
visit(sourceDir);
|
|
150
|
+
return entries;
|
|
151
|
+
}
|
|
152
|
+
function computeFileCrc32(filePath) {
|
|
153
|
+
const fd = fs.openSync(filePath, 'r');
|
|
154
|
+
const buffer = Buffer.alloc(CHUNK_SIZE);
|
|
155
|
+
let crc = 0xffffffff;
|
|
156
|
+
try {
|
|
157
|
+
while (true) {
|
|
158
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
159
|
+
if (bytesRead <= 0)
|
|
160
|
+
break;
|
|
161
|
+
crc = updateCrc32(crc, buffer.subarray(0, bytesRead));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
fs.closeSync(fd);
|
|
166
|
+
}
|
|
167
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
168
|
+
}
|
|
169
|
+
function writeFileData(fd, filePath) {
|
|
170
|
+
const sourceFd = fs.openSync(filePath, 'r');
|
|
171
|
+
const buffer = Buffer.alloc(CHUNK_SIZE);
|
|
172
|
+
try {
|
|
173
|
+
while (true) {
|
|
174
|
+
const bytesRead = fs.readSync(sourceFd, buffer, 0, buffer.length, null);
|
|
175
|
+
if (bytesRead <= 0)
|
|
176
|
+
break;
|
|
177
|
+
fs.writeSync(fd, buffer.subarray(0, bytesRead));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
fs.closeSync(sourceFd);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export function createZipArchive(sourceDir, outputPath, options = {}) {
|
|
185
|
+
const sourceStat = fs.statSync(sourceDir);
|
|
186
|
+
if (!sourceStat.isDirectory())
|
|
187
|
+
throw new Error('Not a directory');
|
|
188
|
+
const limits = {
|
|
189
|
+
maxFiles: options.maxFiles ?? DEFAULT_MAX_FILES,
|
|
190
|
+
maxTotalSize: options.maxTotalSize ?? DEFAULT_MAX_TOTAL_SIZE,
|
|
191
|
+
};
|
|
192
|
+
const scanned = collectEntries(sourceDir, limits);
|
|
193
|
+
const zipEntries = [];
|
|
194
|
+
let fileCount = 0;
|
|
195
|
+
let totalSize = 0;
|
|
196
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
197
|
+
const fd = fs.openSync(outputPath, 'w');
|
|
198
|
+
try {
|
|
199
|
+
for (const entry of scanned) {
|
|
200
|
+
const offset = fs.fstatSync(fd).size;
|
|
201
|
+
const nextEntry = { ...entry, offset };
|
|
202
|
+
writeLocalHeader(fd, nextEntry);
|
|
203
|
+
if (nextEntry.sourcePath) {
|
|
204
|
+
writeFileData(fd, nextEntry.sourcePath);
|
|
205
|
+
fileCount += 1;
|
|
206
|
+
totalSize += nextEntry.size;
|
|
207
|
+
}
|
|
208
|
+
zipEntries.push(nextEntry);
|
|
209
|
+
}
|
|
210
|
+
const centralOffset = fs.fstatSync(fd).size;
|
|
211
|
+
for (const entry of zipEntries)
|
|
212
|
+
writeCentralDirectoryHeader(fd, entry);
|
|
213
|
+
const centralSize = fs.fstatSync(fd).size - centralOffset;
|
|
214
|
+
writeEndOfCentralDirectory(fd, zipEntries.length, centralSize, centralOffset);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
fs.closeSync(fd);
|
|
218
|
+
}
|
|
219
|
+
return { outputPath, fileCount, totalSize };
|
|
220
|
+
}
|
|
@@ -1,2 +1,136 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// @arch docs/architecture/data-sync.md#删除闭环
|
|
2
|
+
// @test src/__tests__/session-deletion-sync.test.ts
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { resolvePersonalSyncV1Path } from '../personal-sync/paths.js';
|
|
7
|
+
import { getDaemonInstallationId } from '../personal-sync/identity.js';
|
|
8
|
+
import { recordPersonalSyncLocalMetric } from '../personal-sync/metrics.js';
|
|
9
|
+
export class DeletionSyncTerminalError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
retryable = false;
|
|
12
|
+
constructor(code) {
|
|
13
|
+
super(`session deletion sync stopped: ${code}`);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = 'DeletionSyncTerminalError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class DeletionSyncRetryWaitError extends Error {
|
|
19
|
+
retryAfterMs;
|
|
20
|
+
retryable = true;
|
|
21
|
+
constructor(retryAfterMs) {
|
|
22
|
+
super(`session deletion sync retry delayed by ${retryAfterMs}ms`);
|
|
23
|
+
this.retryAfterMs = retryAfterMs;
|
|
24
|
+
this.name = 'DeletionSyncRetryWaitError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function readDeletionSyncRevision() {
|
|
28
|
+
return readState().lastAppliedRevision;
|
|
29
|
+
}
|
|
30
|
+
function readState() {
|
|
31
|
+
const file = stateFile();
|
|
32
|
+
if (!fs.existsSync(file))
|
|
33
|
+
return {
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
daemonInstallationId: getDaemonInstallationId(),
|
|
36
|
+
lastAppliedRevision: 0,
|
|
37
|
+
updatedAt: new Date(0).toISOString(),
|
|
38
|
+
};
|
|
39
|
+
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
40
|
+
if (state.schemaVersion !== 1
|
|
41
|
+
|| state.daemonInstallationId !== getDaemonInstallationId()
|
|
42
|
+
|| !Number.isSafeInteger(state.lastAppliedRevision)
|
|
43
|
+
|| state.lastAppliedRevision < 0)
|
|
44
|
+
throw new Error('invalid deletion sync state');
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
export function writeDeletionSyncRevision(revision) {
|
|
48
|
+
if (!Number.isSafeInteger(revision) || revision < 0)
|
|
49
|
+
throw new Error('invalid deletion sync revision');
|
|
50
|
+
const current = readDeletionSyncRevision();
|
|
51
|
+
if (revision < current)
|
|
52
|
+
throw new Error('deletion sync revision cannot move backwards');
|
|
53
|
+
if (revision === current)
|
|
54
|
+
return;
|
|
55
|
+
atomicWriteJson(stateFile(), {
|
|
56
|
+
...readState(),
|
|
57
|
+
lastAppliedRevision: revision,
|
|
58
|
+
updatedAt: new Date().toISOString(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function assertDeletionSyncRunnable(now = Date.now()) {
|
|
62
|
+
const state = readState();
|
|
63
|
+
if (state.terminalCode)
|
|
64
|
+
throw new DeletionSyncTerminalError(state.terminalCode);
|
|
65
|
+
if (state.nextAttemptAt) {
|
|
66
|
+
const remaining = Date.parse(state.nextAttemptAt) - now;
|
|
67
|
+
if (remaining > 0)
|
|
68
|
+
throw new DeletionSyncRetryWaitError(remaining);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function recordDeletionSyncFailure(input) {
|
|
72
|
+
const state = readState();
|
|
73
|
+
const attempts = (state.retryAttempts ?? 0) + 1;
|
|
74
|
+
const now = input.now ?? Date.now();
|
|
75
|
+
if (!input.retryable || attempts >= (input.maxAttempts ?? 8)) {
|
|
76
|
+
atomicWriteJson(stateFile(), {
|
|
77
|
+
...state, retryAttempts: attempts, nextAttemptAt: null,
|
|
78
|
+
terminalCode: input.code, updatedAt: new Date(now).toISOString(),
|
|
79
|
+
});
|
|
80
|
+
recordPersonalSyncLocalMetric('permanentStops');
|
|
81
|
+
throw new DeletionSyncTerminalError(input.code);
|
|
82
|
+
}
|
|
83
|
+
const delay = Math.min(5_000 * 2 ** Math.max(0, attempts - 1), 6 * 60 * 60 * 1_000);
|
|
84
|
+
const jitter = Math.round(delay * 0.2 * (Math.random() * 2 - 1));
|
|
85
|
+
const retryAfterMs = delay + jitter;
|
|
86
|
+
atomicWriteJson(stateFile(), {
|
|
87
|
+
...state, retryAttempts: attempts, terminalCode: null,
|
|
88
|
+
nextAttemptAt: new Date(now + retryAfterMs).toISOString(),
|
|
89
|
+
updatedAt: new Date(now).toISOString(),
|
|
90
|
+
});
|
|
91
|
+
recordPersonalSyncLocalMetric('transientRetries');
|
|
92
|
+
throw new DeletionSyncRetryWaitError(retryAfterMs);
|
|
93
|
+
}
|
|
94
|
+
export function clearDeletionSyncFailure() {
|
|
95
|
+
const state = readState();
|
|
96
|
+
if (!state.retryAttempts && !state.nextAttemptAt && !state.terminalCode)
|
|
97
|
+
return;
|
|
98
|
+
atomicWriteJson(stateFile(), {
|
|
99
|
+
...state, retryAttempts: 0, nextAttemptAt: null, terminalCode: null,
|
|
100
|
+
updatedAt: new Date().toISOString(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function stateFile() {
|
|
104
|
+
return resolvePersonalSyncV1Path('deletion-sync-state.json');
|
|
105
|
+
}
|
|
106
|
+
function atomicWriteJson(file, value) {
|
|
107
|
+
const directory = path.dirname(file);
|
|
108
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
109
|
+
const temp = `${file}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
110
|
+
let fd;
|
|
111
|
+
try {
|
|
112
|
+
fd = fs.openSync(temp, 'wx', 0o600);
|
|
113
|
+
fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
114
|
+
fs.fsyncSync(fd);
|
|
115
|
+
fs.closeSync(fd);
|
|
116
|
+
fd = undefined;
|
|
117
|
+
fs.renameSync(temp, file);
|
|
118
|
+
if (process.platform !== 'win32') {
|
|
119
|
+
const directoryFd = fs.openSync(directory, 'r');
|
|
120
|
+
try {
|
|
121
|
+
fs.fsyncSync(directoryFd);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
fs.closeSync(directoryFd);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (fd !== undefined)
|
|
130
|
+
fs.closeSync(fd);
|
|
131
|
+
try {
|
|
132
|
+
fs.unlinkSync(temp);
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -1 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/features/agent-native-capabilities.md
|
|
2
|
+
// @test src/__tests__/native-capabilities.test.ts
|
|
3
|
+
import { isNativeCapabilityProvider, listAgentNativeCapabilities } from '../../agents/native-capabilities.js';
|
|
4
|
+
export async function handleAgentCapabilitiesList(runtime, req) {
|
|
5
|
+
const agentType = typeof req.params.agentType === 'string' ? req.params.agentType : '';
|
|
6
|
+
const workDir = typeof req.params.workDir === 'string' ? req.params.workDir : process.cwd();
|
|
7
|
+
const supported = isNativeCapabilityProvider(agentType);
|
|
8
|
+
const payload = {
|
|
9
|
+
supported,
|
|
10
|
+
agentType,
|
|
11
|
+
capabilities: supported ? listAgentNativeCapabilities({ agentType, workDir }) : [],
|
|
12
|
+
};
|
|
13
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload });
|
|
14
|
+
}
|