shennian 0.3.84 → 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 +30 -30
- 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,15 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/architecture/data-sync.md#8-发送确认与离线边界
|
|
2
|
+
// @test src/__tests__/session-store.test.ts
|
|
3
|
+
import { getMessageDeliveryStatus } from '../store.js';
|
|
4
|
+
import { assertPersonalSyncIdentity } from '../../personal-sync/epoch.js';
|
|
5
|
+
export function handleSessionMessageStatus(runtime, req) {
|
|
6
|
+
const params = req.params;
|
|
7
|
+
assertPersonalSyncIdentity(params);
|
|
8
|
+
if (!params.sessionId || !params.clientMessageId) {
|
|
9
|
+
throw new Error('sessionId and clientMessageId are required');
|
|
10
|
+
}
|
|
11
|
+
runtime.client.sendRes({
|
|
12
|
+
type: 'res', id: req.id, ok: true,
|
|
13
|
+
payload: getMessageDeliveryStatus(params.sessionId, params.clientMessageId),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -1 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/features/platform-security-boundary-and-hardening-todo.md
|
|
2
|
+
// @test src/__tests__/remote-access-pause.test.ts
|
|
3
|
+
export async function handleRemoteAccessPause(client, controller, req) {
|
|
4
|
+
if (!controller) {
|
|
5
|
+
await client.sendResAndWait({
|
|
6
|
+
type: 'res',
|
|
7
|
+
id: req.id,
|
|
8
|
+
ok: false,
|
|
9
|
+
error: 'upgrade_required',
|
|
10
|
+
}).catch(() => { });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const preparation = controller.prepare();
|
|
15
|
+
if (!preparation.autoStartDisabled) {
|
|
16
|
+
throw new Error(preparation.autoStartError
|
|
17
|
+
? `remote_access_autostart_disable_failed: ${preparation.autoStartError}`
|
|
18
|
+
: 'remote_access_autostart_disable_failed');
|
|
19
|
+
}
|
|
20
|
+
const acknowledgement = await client.sendBufferedEvent({
|
|
21
|
+
type: 'event',
|
|
22
|
+
id: `remote-access-paused-${req.id}`,
|
|
23
|
+
event: 'machine.remote-access.paused',
|
|
24
|
+
payload: { disabledAt: preparation.disabledAt },
|
|
25
|
+
}, 30_000);
|
|
26
|
+
const payload = acknowledgement && typeof acknowledgement.payload === 'object'
|
|
27
|
+
? acknowledgement.payload
|
|
28
|
+
: { remoteAccessDisabledAt: preparation.disabledAt };
|
|
29
|
+
await client.sendResAndWait({ type: 'res', id: req.id, ok: true, payload });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
await client.sendResAndWait({
|
|
33
|
+
type: 'res',
|
|
34
|
+
id: req.id,
|
|
35
|
+
ok: false,
|
|
36
|
+
error: error instanceof Error ? error.message : String(error),
|
|
37
|
+
}).catch(() => { });
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (controller.isDisabled())
|
|
41
|
+
controller.shutdownAfterResponse();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1 +1,101 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/features/network-rooms/multi-session-client-and-room-panel.md
|
|
2
|
+
// @test src/__tests__/managed-room-binding.test.ts
|
|
3
|
+
import { isAvailableAgentType } from '@shennian/wire';
|
|
4
|
+
const SAFE_ID = /^[A-Za-z0-9_.:@/-]{1,191}$/;
|
|
5
|
+
const BIND_PARAMS = new Set([
|
|
6
|
+
'roomId',
|
|
7
|
+
'agentIdentityId',
|
|
8
|
+
'nianSessionId',
|
|
9
|
+
'assignmentIntentId',
|
|
10
|
+
'agentType',
|
|
11
|
+
]);
|
|
12
|
+
const RESTART_PARAMS = new Set([
|
|
13
|
+
'roomId',
|
|
14
|
+
'bindingId',
|
|
15
|
+
'agentIdentityId',
|
|
16
|
+
'previousNianSessionId',
|
|
17
|
+
'nianSessionId',
|
|
18
|
+
'assignmentIntentId',
|
|
19
|
+
'agentType',
|
|
20
|
+
]);
|
|
21
|
+
export async function handleManagedRoomBind(runtime, binder, req) {
|
|
22
|
+
if (!binder)
|
|
23
|
+
throw new Error('Managed Room binding is unavailable.');
|
|
24
|
+
if (Object.keys(req.params).some((key) => !BIND_PARAMS.has(key))) {
|
|
25
|
+
throw new Error('Managed Room binding contains unsupported parameters.');
|
|
26
|
+
}
|
|
27
|
+
const roomId = requireId(req.params.roomId, 'roomId');
|
|
28
|
+
const agentIdentityId = requireId(req.params.agentIdentityId, 'agentIdentityId');
|
|
29
|
+
const nianSessionId = requireId(req.params.nianSessionId, 'nianSessionId');
|
|
30
|
+
const assignmentIntentId = requireId(req.params.assignmentIntentId, 'assignmentIntentId');
|
|
31
|
+
const agentType = requireManagedAgentType(req.params.agentType);
|
|
32
|
+
await discardPreBindingSession(runtime, nianSessionId);
|
|
33
|
+
const result = await binder.bind({
|
|
34
|
+
roomId,
|
|
35
|
+
agentIdentityId,
|
|
36
|
+
nianSessionId,
|
|
37
|
+
assignmentIntentId,
|
|
38
|
+
sourceSessionKey: managedSourceSessionKey(nianSessionId),
|
|
39
|
+
agentType,
|
|
40
|
+
});
|
|
41
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: result });
|
|
42
|
+
}
|
|
43
|
+
export async function handleManagedRoomRestart(runtime, binder, req) {
|
|
44
|
+
if (!binder)
|
|
45
|
+
throw new Error('Managed Room binding is unavailable.');
|
|
46
|
+
if (Object.keys(req.params).some((key) => !RESTART_PARAMS.has(key))) {
|
|
47
|
+
throw new Error('Managed Room restart contains unsupported parameters.');
|
|
48
|
+
}
|
|
49
|
+
const roomId = requireId(req.params.roomId, 'roomId');
|
|
50
|
+
const bindingId = requireId(req.params.bindingId, 'bindingId');
|
|
51
|
+
const agentIdentityId = requireId(req.params.agentIdentityId, 'agentIdentityId');
|
|
52
|
+
const previousNianSessionId = requireId(req.params.previousNianSessionId, 'previousNianSessionId');
|
|
53
|
+
const nianSessionId = requireId(req.params.nianSessionId, 'nianSessionId');
|
|
54
|
+
const assignmentIntentId = requireId(req.params.assignmentIntentId, 'assignmentIntentId');
|
|
55
|
+
const agentType = requireManagedAgentType(req.params.agentType);
|
|
56
|
+
await discardPreBindingSession(runtime, nianSessionId);
|
|
57
|
+
const result = await binder.restart({
|
|
58
|
+
roomId,
|
|
59
|
+
bindingId,
|
|
60
|
+
agentIdentityId,
|
|
61
|
+
previousNianSessionId,
|
|
62
|
+
nianSessionId,
|
|
63
|
+
assignmentIntentId,
|
|
64
|
+
sourceSessionKey: managedSourceSessionKey(nianSessionId),
|
|
65
|
+
agentType,
|
|
66
|
+
});
|
|
67
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: result });
|
|
68
|
+
}
|
|
69
|
+
function requireId(value, field) {
|
|
70
|
+
if (typeof value !== 'string' || !SAFE_ID.test(value)) {
|
|
71
|
+
throw new Error(`Invalid ${field}.`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function requireManagedAgentType(value) {
|
|
76
|
+
if (typeof value !== 'string' ||
|
|
77
|
+
!isAvailableAgentType(value) ||
|
|
78
|
+
value === 'manager' ||
|
|
79
|
+
value === 'pi' ||
|
|
80
|
+
value === 'openclaw') {
|
|
81
|
+
throw new Error('managed_agent_runtime_unsupported');
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function managedSourceSessionKey(nianSessionId) {
|
|
86
|
+
return `managed:${nianSessionId}`;
|
|
87
|
+
}
|
|
88
|
+
async function discardPreBindingSession(runtime, nianSessionId) {
|
|
89
|
+
const session = runtime.sessions.get(nianSessionId);
|
|
90
|
+
if (!session)
|
|
91
|
+
return;
|
|
92
|
+
runtime.sessions.delete(nianSessionId);
|
|
93
|
+
if (session.managedRoomContextToken) {
|
|
94
|
+
runtime.revokeManagedRoomContextToken?.(session.managedRoomContextToken);
|
|
95
|
+
session.managedRoomContextToken = null;
|
|
96
|
+
}
|
|
97
|
+
if (session.heartbeatTimer)
|
|
98
|
+
clearInterval(session.heartbeatTimer);
|
|
99
|
+
session.adapter.removeAllListeners();
|
|
100
|
+
await session.adapter.stop();
|
|
101
|
+
}
|
|
@@ -1 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/architecture/cli/native-session-fusion.md
|
|
2
|
+
// @test src/__tests__/session-manager.test.ts
|
|
3
|
+
export async function handleSessionRefresh(runtime, req) {
|
|
4
|
+
const params = req.params;
|
|
5
|
+
if (!params.sessionId) {
|
|
6
|
+
runtime.client.sendRes({
|
|
7
|
+
type: 'res',
|
|
8
|
+
id: req.id,
|
|
9
|
+
ok: false,
|
|
10
|
+
error: 'sessionId is required',
|
|
11
|
+
});
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!runtime.nativeFusion) {
|
|
15
|
+
runtime.client.sendRes({
|
|
16
|
+
type: 'res',
|
|
17
|
+
id: req.id,
|
|
18
|
+
ok: true,
|
|
19
|
+
payload: { scanned: false },
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await runtime.nativeFusion.scanNow();
|
|
24
|
+
runtime.client.sendRes({
|
|
25
|
+
type: 'res',
|
|
26
|
+
id: req.id,
|
|
27
|
+
ok: true,
|
|
28
|
+
payload: {
|
|
29
|
+
scanned: true,
|
|
30
|
+
sessionId: params.sessionId,
|
|
31
|
+
agentType: params.agentType ?? null,
|
|
32
|
+
agentSessionId: params.agentSessionId ?? null,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -1 +1,79 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/architecture/cli/daemon.md#会话管理
|
|
2
|
+
// @test src/__tests__/session-manager.test.ts
|
|
3
|
+
import { createAgent } from '../../agents/adapter.js';
|
|
4
|
+
import { buildManagedAgentEnv } from '../../agents/config-status.js';
|
|
5
|
+
import { recordSession } from '../store.js';
|
|
6
|
+
import { requestSessionIndexPublish } from '../index-publisher.js';
|
|
7
|
+
import { assertPersonalSyncIdentity } from '../../personal-sync/epoch.js';
|
|
8
|
+
function cleanString(value) {
|
|
9
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
10
|
+
}
|
|
11
|
+
export async function handleSessionTitleSet(runtime, req) {
|
|
12
|
+
const params = req.params;
|
|
13
|
+
assertPersonalSyncIdentity(params);
|
|
14
|
+
const sessionId = cleanString(params.sessionId);
|
|
15
|
+
const title = cleanString(params.title);
|
|
16
|
+
const agentSessionId = cleanString(params.agentSessionId);
|
|
17
|
+
const agentType = cleanString(params.agentType);
|
|
18
|
+
const workDir = cleanString(params.workDir) || process.cwd();
|
|
19
|
+
const titleOriginEventId = cleanString(params.titleOriginEventId);
|
|
20
|
+
const titleRevision = params.titleRevision;
|
|
21
|
+
if (!sessionId || !title || !agentType || params.titleSource !== 'shennian_manual'
|
|
22
|
+
|| !Number.isSafeInteger(titleRevision) || Number(titleRevision) < 1 || !titleOriginEventId) {
|
|
23
|
+
runtime.client.sendRes({
|
|
24
|
+
type: 'res',
|
|
25
|
+
id: req.id,
|
|
26
|
+
ok: false,
|
|
27
|
+
error: 'explicit shennian_manual title authority is required',
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
recordSession({
|
|
32
|
+
sessionId,
|
|
33
|
+
agentType,
|
|
34
|
+
workDir,
|
|
35
|
+
agentSessionId: agentSessionId || null,
|
|
36
|
+
title,
|
|
37
|
+
titleSource: 'shennian_manual',
|
|
38
|
+
titleRevision: Number(titleRevision),
|
|
39
|
+
titleLockedByUser: true,
|
|
40
|
+
titleOriginEventId,
|
|
41
|
+
});
|
|
42
|
+
requestSessionIndexPublish(runtime.client);
|
|
43
|
+
if (agentType !== 'codex' || !agentSessionId) {
|
|
44
|
+
runtime.client.sendRes({
|
|
45
|
+
type: 'res',
|
|
46
|
+
id: req.id,
|
|
47
|
+
ok: true,
|
|
48
|
+
payload: { localApplied: true, nativeWriteback: 'unsupported' },
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const active = runtime.sessions.get(sessionId);
|
|
53
|
+
if (active?.agentType === 'codex' && active.adapter.setTitle) {
|
|
54
|
+
await active.adapter.setTitle(agentSessionId, title, active.workDir || workDir);
|
|
55
|
+
active.agentSessionId = agentSessionId;
|
|
56
|
+
active.lastActiveAt = Date.now();
|
|
57
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { synced: true } });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const adapter = createAgent('codex');
|
|
61
|
+
if (!adapter?.setTitle) {
|
|
62
|
+
runtime.client.sendRes({
|
|
63
|
+
type: 'res',
|
|
64
|
+
id: req.id,
|
|
65
|
+
ok: false,
|
|
66
|
+
error: 'Codex adapter does not support title sync',
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
adapter.configure?.({ sessionId, env: buildManagedAgentEnv('codex') });
|
|
71
|
+
try {
|
|
72
|
+
await adapter.start(sessionId, workDir, agentSessionId);
|
|
73
|
+
await adapter.setTitle(agentSessionId, title, workDir);
|
|
74
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { synced: true } });
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await adapter.stop().catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1 +1,149 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/architecture/data-sync.md#工具详情按需读取
|
|
2
|
+
// @test src/__tests__/session-manager.test.ts
|
|
3
|
+
import { isSystemControlPayload, SESSION_TOOL_DETAIL_LIMITS, } from '@shennian/wire';
|
|
4
|
+
import { readToolDetail } from '../tool-detail-store.js';
|
|
5
|
+
import { assertPersonalSyncIdentity, getPersonalSyncIdentity } from '../../personal-sync/epoch.js';
|
|
6
|
+
import { getDaemonInstallationId } from '../store.js';
|
|
7
|
+
const SENSITIVE_KEY_RE = /(?:token|api[-_]?key|password|passwd|pwd|secret|authorization|cookie|credential|private[-_]?key)/i;
|
|
8
|
+
const SENSITIVE_ASSIGNMENT_RE = /\b([A-Z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD|PASSWD|PWD|AUTH)[A-Z0-9_]*)=("[^"]*"|'[^']*'|[^\s]+)/gi;
|
|
9
|
+
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
10
|
+
const OPENAI_KEY_RE = /\bsk-[A-Za-z0-9_-]{12,}\b/g;
|
|
11
|
+
function asRecord(value) {
|
|
12
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
: null;
|
|
15
|
+
}
|
|
16
|
+
function clampMaxBytes(value) {
|
|
17
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
18
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
19
|
+
return SESSION_TOOL_DETAIL_LIMITS.defaultMaxBytes;
|
|
20
|
+
return Math.min(Math.max(SESSION_TOOL_DETAIL_LIMITS.minMaxBytes, Math.floor(parsed)), SESSION_TOOL_DETAIL_LIMITS.maxMaxBytes);
|
|
21
|
+
}
|
|
22
|
+
function redactString(value, maxChars) {
|
|
23
|
+
if (isSystemControlPayload(value))
|
|
24
|
+
return '[SYSTEM CONTEXT REDACTED]';
|
|
25
|
+
const redacted = value
|
|
26
|
+
.replace(SENSITIVE_ASSIGNMENT_RE, '$1=[REDACTED]')
|
|
27
|
+
.replace(BEARER_RE, 'Bearer [REDACTED]')
|
|
28
|
+
.replace(OPENAI_KEY_RE, 'sk-[REDACTED]');
|
|
29
|
+
return redacted.length > maxChars ? `${redacted.slice(0, maxChars)}…` : redacted;
|
|
30
|
+
}
|
|
31
|
+
function sanitizeValue(value, maxChars, depth = 0) {
|
|
32
|
+
if (value == null)
|
|
33
|
+
return value;
|
|
34
|
+
if (typeof value === 'string')
|
|
35
|
+
return redactString(value, maxChars);
|
|
36
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
37
|
+
return value;
|
|
38
|
+
if (depth >= 4)
|
|
39
|
+
return '[Truncated]';
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return value.slice(0, 20).map((item) => sanitizeValue(item, maxChars, depth + 1));
|
|
42
|
+
if (typeof value === 'object') {
|
|
43
|
+
const output = {};
|
|
44
|
+
for (const [key, raw] of Object.entries(value).slice(0, 50)) {
|
|
45
|
+
output[key] = SENSITIVE_KEY_RE.test(key) ? '[REDACTED]' : sanitizeValue(raw, maxChars, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
function extractCommand(args, maxChars) {
|
|
52
|
+
const raw = asRecord(args)?.command;
|
|
53
|
+
if (typeof raw === 'string')
|
|
54
|
+
return redactString(raw, maxChars);
|
|
55
|
+
if (Array.isArray(raw))
|
|
56
|
+
return redactString(raw.map(String).join(' '), maxChars);
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function byteSize(value) {
|
|
60
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
61
|
+
}
|
|
62
|
+
function finalizeByteSize(payload) {
|
|
63
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
64
|
+
const next = byteSize(payload);
|
|
65
|
+
if (next === payload.byteSize)
|
|
66
|
+
break;
|
|
67
|
+
payload.byteSize = next;
|
|
68
|
+
}
|
|
69
|
+
return payload;
|
|
70
|
+
}
|
|
71
|
+
function unavailable(request, reason) {
|
|
72
|
+
const payload = {
|
|
73
|
+
...getPersonalSyncIdentity(getDaemonInstallationId()),
|
|
74
|
+
sessionId: request.sessionId,
|
|
75
|
+
toolCallId: request.toolCallId,
|
|
76
|
+
detailRevision: request.detailRevision,
|
|
77
|
+
detailStatus: reason === 'not_found' ? 'not_found' : 'unavailable',
|
|
78
|
+
resultOmitted: true,
|
|
79
|
+
redacted: true,
|
|
80
|
+
byteSize: 0,
|
|
81
|
+
expiresAt: new Date(Date.now() + SESSION_TOOL_DETAIL_LIMITS.responseTtlMs).toISOString(),
|
|
82
|
+
source: 'unavailable',
|
|
83
|
+
reason,
|
|
84
|
+
};
|
|
85
|
+
return finalizeByteSize(payload);
|
|
86
|
+
}
|
|
87
|
+
export async function handleSessionToolDetail(runtime, req) {
|
|
88
|
+
const params = req.params;
|
|
89
|
+
assertPersonalSyncIdentity(params);
|
|
90
|
+
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : '';
|
|
91
|
+
const toolCallId = typeof params.toolCallId === 'string' ? params.toolCallId : '';
|
|
92
|
+
const detailRevision = typeof params.detailRevision === 'number' && Number.isSafeInteger(params.detailRevision)
|
|
93
|
+
? params.detailRevision
|
|
94
|
+
: -1;
|
|
95
|
+
if (!sessionId || !toolCallId || detailRevision < 0) {
|
|
96
|
+
runtime.client.sendRes({
|
|
97
|
+
type: 'res', id: req.id, ok: false,
|
|
98
|
+
error: 'upgrade_required: sessionId, toolCallId and detailRevision are required',
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const maxBytes = clampMaxBytes(params.maxBytes);
|
|
103
|
+
const request = {
|
|
104
|
+
...getPersonalSyncIdentity(getDaemonInstallationId()), sessionId, toolCallId, detailRevision, maxBytes,
|
|
105
|
+
};
|
|
106
|
+
const detail = readToolDetail(sessionId, toolCallId);
|
|
107
|
+
if (!detail) {
|
|
108
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: unavailable(request, 'not_found') });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (detail.detailRevision !== detailRevision) {
|
|
112
|
+
runtime.client.sendRes({
|
|
113
|
+
type: 'res', id: req.id, ok: true, payload: unavailable(request, 'revision_mismatch'),
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const maxChars = Math.max(512, Math.floor(maxBytes / 4));
|
|
118
|
+
const argsSummary = detail.args == null ? undefined : sanitizeValue(detail.args, maxChars);
|
|
119
|
+
const command = extractCommand(detail.args, maxChars);
|
|
120
|
+
const result = typeof detail.result === 'string'
|
|
121
|
+
? redactString(detail.result, maxChars)
|
|
122
|
+
: detail.result == null
|
|
123
|
+
? undefined
|
|
124
|
+
: redactString(JSON.stringify(sanitizeValue(detail.result, maxChars)), maxChars);
|
|
125
|
+
const expiresAt = new Date(Date.now() + SESSION_TOOL_DETAIL_LIMITS.responseTtlMs).toISOString();
|
|
126
|
+
let payload = {
|
|
127
|
+
...getPersonalSyncIdentity(getDaemonInstallationId()),
|
|
128
|
+
sessionId, toolCallId, detailRevision,
|
|
129
|
+
detailStatus: 'available',
|
|
130
|
+
status: detail.status,
|
|
131
|
+
...(command ? { command } : {}),
|
|
132
|
+
...(argsSummary !== undefined ? { argsSummary } : {}),
|
|
133
|
+
...(result !== undefined ? { result } : {}),
|
|
134
|
+
resultOmitted: result === undefined,
|
|
135
|
+
redacted: true,
|
|
136
|
+
byteSize: 0,
|
|
137
|
+
expiresAt,
|
|
138
|
+
source: 'local-tool-index',
|
|
139
|
+
};
|
|
140
|
+
finalizeByteSize(payload);
|
|
141
|
+
if (payload.byteSize > maxBytes) {
|
|
142
|
+
delete payload.result;
|
|
143
|
+
payload.resultOmitted = true;
|
|
144
|
+
finalizeByteSize(payload);
|
|
145
|
+
}
|
|
146
|
+
if (payload.byteSize > maxBytes)
|
|
147
|
+
payload = unavailable(request, 'response_too_large');
|
|
148
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload });
|
|
149
|
+
}
|
|
@@ -1 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/architecture/data-sync.md#6-voice-shennian-边界
|
|
2
|
+
// @test ../../__tests__/voice-window.test.ts
|
|
3
|
+
import { extractPayloadText, isAgentHiddenPayload, isSystemControlPayload, isToolPayload, } from '@shennian/wire';
|
|
4
|
+
import { getCanonicalSessionStore, getDaemonInstallationId } from '../store.js';
|
|
5
|
+
import { assertPersonalSyncIdentity, getPersonalSyncIdentity } from '../../personal-sync/epoch.js';
|
|
6
|
+
const MAX_MESSAGES = 8;
|
|
7
|
+
const MAX_BYTES = 32 * 1024;
|
|
8
|
+
export function handleVoiceSessionBodyWindow(runtime, req) {
|
|
9
|
+
const params = req.params;
|
|
10
|
+
assertPersonalSyncIdentity(params);
|
|
11
|
+
if (!params?.sessionId?.trim() || !params.callId?.trim()) {
|
|
12
|
+
throw new Error('sessionId and callId are required');
|
|
13
|
+
}
|
|
14
|
+
const limit = boundedInteger(params.limit, 4, MAX_MESSAGES);
|
|
15
|
+
const maxBytes = boundedInteger(params.maxBytes, MAX_BYTES, MAX_BYTES);
|
|
16
|
+
const page = getCanonicalSessionStore().readRecent(params.sessionId, Math.min(limit * 6, 48));
|
|
17
|
+
const visible = page.messages.filter(isVoiceReadableMessage);
|
|
18
|
+
const selected = [];
|
|
19
|
+
let bytes = 0;
|
|
20
|
+
for (let index = visible.length - 1; index >= 0 && selected.length < limit; index -= 1) {
|
|
21
|
+
const message = visible[index];
|
|
22
|
+
const size = Buffer.byteLength(JSON.stringify(message), 'utf8');
|
|
23
|
+
if (selected.length > 0 && bytes + size > maxBytes)
|
|
24
|
+
break;
|
|
25
|
+
if (size > maxBytes)
|
|
26
|
+
continue;
|
|
27
|
+
selected.push(message);
|
|
28
|
+
bytes += size;
|
|
29
|
+
}
|
|
30
|
+
selected.reverse();
|
|
31
|
+
runtime.client.sendRes({
|
|
32
|
+
type: 'res',
|
|
33
|
+
id: req.id,
|
|
34
|
+
ok: true,
|
|
35
|
+
payload: {
|
|
36
|
+
...getPersonalSyncIdentity(getDaemonInstallationId()),
|
|
37
|
+
sessionId: params.sessionId,
|
|
38
|
+
callId: params.callId,
|
|
39
|
+
bodyRevision: page.bodyRevision,
|
|
40
|
+
messages: selected,
|
|
41
|
+
truncated: page.hasOlder || selected.length < visible.length,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function isVoiceReadableMessage(message) {
|
|
46
|
+
if (message.role !== 'user' && message.role !== 'agent')
|
|
47
|
+
return false;
|
|
48
|
+
if (isToolPayload(message.payload))
|
|
49
|
+
return false;
|
|
50
|
+
if (isSystemControlPayload(message.payload))
|
|
51
|
+
return false;
|
|
52
|
+
if (isAgentHiddenPayload(message.payload))
|
|
53
|
+
return false;
|
|
54
|
+
return extractPayloadText(message.payload).trim().length > 0;
|
|
55
|
+
}
|
|
56
|
+
function boundedInteger(value, fallback, max) {
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
return fallback;
|
|
59
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
60
|
+
throw new Error('invalid voice window limit');
|
|
61
|
+
return Math.min(value, max);
|
|
62
|
+
}
|