evolcore 0.0.2 → 0.0.4
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/CHANGELOG.md +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function sendOperationId(envelope) {
|
|
2
|
+
return envelope.operationId || envelope.taskId;
|
|
3
|
+
}
|
|
4
|
+
export function sentReceipt(envelope, messageIds, threadId) {
|
|
5
|
+
const messages = messageIds
|
|
6
|
+
.filter((messageId) => typeof messageId === 'string' && messageId.length > 0)
|
|
7
|
+
.map((messageId, partIndex) => ({ messageId, partIndex, threadId }));
|
|
8
|
+
if (messages.length === 0) {
|
|
9
|
+
throw new Error(`MISSING_MESSAGE_ID: operation=${sendOperationId(envelope)}`);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
status: 'sent',
|
|
13
|
+
operationId: sendOperationId(envelope),
|
|
14
|
+
messages: messages,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function suppressedReceipt(envelope, reason) {
|
|
18
|
+
return {
|
|
19
|
+
status: 'suppressed',
|
|
20
|
+
operationId: sendOperationId(envelope),
|
|
21
|
+
messages: [],
|
|
22
|
+
reason,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -27,9 +27,9 @@ export class StreamDebouncer {
|
|
|
27
27
|
* 否则新建窗口。timer 到期后自动 flush。
|
|
28
28
|
*/
|
|
29
29
|
submit(key, message, enqueue) {
|
|
30
|
-
const { content, images, mentions, messageId, replyContext, ...rest } = message;
|
|
30
|
+
const { content, images, mentions, messageId, sourceMessageIds, replyContext, ...rest } = message;
|
|
31
31
|
return new Promise((resolve, reject) => {
|
|
32
|
-
const entry = { messageId, content, images, mentions, replyContext, rest, resolve, reject };
|
|
32
|
+
const entry = { messageId, sourceMessageIds, content, images, mentions, replyContext, rest, resolve, reject };
|
|
33
33
|
const win = this.pending.get(key);
|
|
34
34
|
if (win) {
|
|
35
35
|
clearTimeout(win.timer);
|
|
@@ -87,12 +87,20 @@ export class StreamDebouncer {
|
|
|
87
87
|
const allImages = [];
|
|
88
88
|
const allMentions = [];
|
|
89
89
|
const contents = [];
|
|
90
|
+
const sourceMessageIds = [];
|
|
91
|
+
const seenSourceMessageIds = new Set();
|
|
90
92
|
for (const e of entries) {
|
|
91
93
|
contents.push(e.content);
|
|
92
94
|
if (e.images)
|
|
93
95
|
allImages.push(...e.images);
|
|
94
96
|
if (e.mentions)
|
|
95
97
|
allMentions.push(...e.mentions);
|
|
98
|
+
for (const id of [...(e.sourceMessageIds ?? []), ...(e.messageId ? [e.messageId] : [])]) {
|
|
99
|
+
if (!seenSourceMessageIds.has(id)) {
|
|
100
|
+
seenSourceMessageIds.add(id);
|
|
101
|
+
sourceMessageIds.push(id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
96
104
|
}
|
|
97
105
|
const last = entries[entries.length - 1];
|
|
98
106
|
// 合并后保留最新一条的 messageId(用于 thought 锚定与中断追踪)
|
|
@@ -110,6 +118,7 @@ export class StreamDebouncer {
|
|
|
110
118
|
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
111
119
|
replyContext: last.replyContext,
|
|
112
120
|
messageId: latestMessageId,
|
|
121
|
+
sourceMessageIds: sourceMessageIds.length > 0 ? sourceMessageIds : undefined,
|
|
113
122
|
};
|
|
114
123
|
const resolves = entries.map(e => e.resolve);
|
|
115
124
|
const rejects = entries.map(e => e.reject);
|
|
@@ -55,10 +55,10 @@ function structuredApprovalAuditSummary(input) {
|
|
|
55
55
|
.slice(0, 16);
|
|
56
56
|
return fields.length > 0 ? `input_fields=${fields.join(',')}` : 'input_withheld';
|
|
57
57
|
}
|
|
58
|
-
function requiredApproverRole(policy
|
|
58
|
+
function requiredApproverRole(policy) {
|
|
59
59
|
if (policy === 'requester')
|
|
60
60
|
return 'requester';
|
|
61
|
-
if (policy === 'agent_owner'
|
|
61
|
+
if (policy === 'agent_owner')
|
|
62
62
|
return 'owner';
|
|
63
63
|
return 'manager';
|
|
64
64
|
}
|
|
@@ -148,13 +148,20 @@ export function planApprovalRoute(challenge, context) {
|
|
|
148
148
|
approverPrincipalId: actor.principalId,
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
const handoffCandidates = challenge.approverPolicy === 'agent_manager'
|
|
152
|
+
? Array.from(new Set([...admins, ...owners]))
|
|
153
|
+
: owners;
|
|
154
|
+
if (handoffCandidates.length === 0) {
|
|
155
|
+
return {
|
|
156
|
+
kind: 'unavailable',
|
|
157
|
+
reason: challenge.approverPolicy === 'agent_manager'
|
|
158
|
+
? 'no_agent_manager_configured'
|
|
159
|
+
: 'no_agent_owner_configured',
|
|
160
|
+
};
|
|
153
161
|
}
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
const approverId = owners[0];
|
|
162
|
+
// Manager handoffs prefer the configured admin pool for routine approvals;
|
|
163
|
+
// owner-only policies remain restricted to owners. Only one card is sent.
|
|
164
|
+
const approverId = handoffCandidates[0];
|
|
158
165
|
if (!approverId) {
|
|
159
166
|
return { kind: 'unavailable', reason: 'no_aun_owner_available' };
|
|
160
167
|
}
|
|
@@ -505,7 +512,7 @@ export class PermissionGateway {
|
|
|
505
512
|
grantScope,
|
|
506
513
|
approverPolicy: challenge.approverPolicy,
|
|
507
514
|
approvalRouteKind: route.kind,
|
|
508
|
-
requiredApproverRole: requiredApproverRole(challenge.approverPolicy
|
|
515
|
+
requiredApproverRole: requiredApproverRole(challenge.approverPolicy),
|
|
509
516
|
approverOperatorId: route.approverOperatorId,
|
|
510
517
|
approverChannelKey: route.approverChannelKey,
|
|
511
518
|
approverPrincipalId: route.approverPrincipalId,
|
|
@@ -763,7 +770,7 @@ export class PermissionGateway {
|
|
|
763
770
|
grantScope,
|
|
764
771
|
approverPolicy: challenge.approverPolicy,
|
|
765
772
|
approvalRouteKind: route.kind,
|
|
766
|
-
requiredApproverRole: requiredApproverRole(challenge.approverPolicy
|
|
773
|
+
requiredApproverRole: requiredApproverRole(challenge.approverPolicy),
|
|
767
774
|
approverOperatorId: route.approverOperatorId,
|
|
768
775
|
approverChannelKey: route.approverChannelKey,
|
|
769
776
|
approverPrincipalId: route.approverPrincipalId,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
const MAX_HEAD_LINES = 10_000;
|
|
1
|
+
const HEAD_EXECUTABLES = new Set(['head', '/bin/head', '/usr/bin/head']);
|
|
3
2
|
const SHELL_UNQUOTED_META = new Set([
|
|
4
3
|
';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
|
|
5
4
|
]);
|
|
@@ -11,16 +10,36 @@ function trimShellSpacing(value) {
|
|
|
11
10
|
function isShellSpacing(value) {
|
|
12
11
|
return value === ' ' || value === '\t';
|
|
13
12
|
}
|
|
13
|
+
function isLiteralArgBoundary(value) {
|
|
14
|
+
return value === undefined || isShellSpacing(value);
|
|
15
|
+
}
|
|
16
|
+
function isShellParameterExpansionStart(value) {
|
|
17
|
+
return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
|
|
18
|
+
}
|
|
19
|
+
function doubleQuotedIssueAt(input, index) {
|
|
20
|
+
const char = input[index];
|
|
21
|
+
if (char === '`' || (char === '$' && isShellParameterExpansionStart(input[index + 1]))) {
|
|
22
|
+
return 'unsafe-expansion';
|
|
23
|
+
}
|
|
24
|
+
if (char !== '\\')
|
|
25
|
+
return null;
|
|
26
|
+
const next = input[index + 1];
|
|
27
|
+
if (next === undefined)
|
|
28
|
+
return 'invalid-quote';
|
|
29
|
+
return next === '\r' || next === '\n' ? 'structural-newline' : null;
|
|
30
|
+
}
|
|
14
31
|
/**
|
|
15
32
|
* Parse the deliberately small shell subset accepted by the privileged EC
|
|
16
33
|
* command path. The returned argv must describe one literal invocation: no
|
|
17
34
|
* expansion, glob, pipeline, command substitution, redirection, or comment.
|
|
18
35
|
* Anything outside this subset is never granted the privileged EC path.
|
|
19
36
|
*/
|
|
20
|
-
export function
|
|
37
|
+
export function parseLiteralShellCommand(command) {
|
|
21
38
|
const input = trimShellSpacing(command);
|
|
22
|
-
if (!input
|
|
23
|
-
return
|
|
39
|
+
if (!input)
|
|
40
|
+
return { ok: false, issue: 'empty-command' };
|
|
41
|
+
if (input.includes('\0'))
|
|
42
|
+
return { ok: false, issue: 'invalid-control-char' };
|
|
24
43
|
const argv = [];
|
|
25
44
|
let token = '';
|
|
26
45
|
let tokenStarted = false;
|
|
@@ -35,26 +54,28 @@ export function parseLiteralShellArgv(command) {
|
|
|
35
54
|
for (let index = 0; index < input.length; index++) {
|
|
36
55
|
const char = input[index];
|
|
37
56
|
if (quote === 'single') {
|
|
38
|
-
if (char === "'")
|
|
57
|
+
if (char === "'") {
|
|
58
|
+
if (!isLiteralArgBoundary(input[index + 1]))
|
|
59
|
+
return { ok: false, issue: 'invalid-quote' };
|
|
39
60
|
quote = null;
|
|
61
|
+
}
|
|
40
62
|
else
|
|
41
63
|
token += char;
|
|
42
64
|
continue;
|
|
43
65
|
}
|
|
44
66
|
if (quote === 'double') {
|
|
45
67
|
if (char === '"') {
|
|
68
|
+
if (!isLiteralArgBoundary(input[index + 1]))
|
|
69
|
+
return { ok: false, issue: 'invalid-quote' };
|
|
46
70
|
quote = null;
|
|
47
71
|
continue;
|
|
48
72
|
}
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
const issue = doubleQuotedIssueAt(input, index);
|
|
74
|
+
if (issue)
|
|
75
|
+
return { ok: false, issue };
|
|
51
76
|
if (char === '\\') {
|
|
52
77
|
const next = input[++index];
|
|
53
|
-
|
|
54
|
-
return null;
|
|
55
|
-
if (next === '$' || next === '`')
|
|
56
|
-
return null;
|
|
57
|
-
token += ['"', '\\'].includes(next) ? next : `\\${next}`;
|
|
78
|
+
token += ['"', '\\', '$', '`'].includes(next) ? next : `\\${next}`;
|
|
58
79
|
continue;
|
|
59
80
|
}
|
|
60
81
|
token += char;
|
|
@@ -70,8 +91,11 @@ export function parseLiteralShellArgv(command) {
|
|
|
70
91
|
// includes NBSP, line/paragraph separators, VT and FF, which the shell
|
|
71
92
|
// keeps inside the executable name; accepting those would misclassify a
|
|
72
93
|
// different executable as `ec`.
|
|
73
|
-
if (char === '\r' || char === '\n'
|
|
74
|
-
return
|
|
94
|
+
if (char === '\r' || char === '\n')
|
|
95
|
+
return { ok: false, issue: 'structural-newline' };
|
|
96
|
+
if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
|
|
97
|
+
return { ok: false, issue: 'invalid-control-char' };
|
|
98
|
+
}
|
|
75
99
|
if (char === "'") {
|
|
76
100
|
tokenStarted = true;
|
|
77
101
|
quote = 'single';
|
|
@@ -84,8 +108,10 @@ export function parseLiteralShellArgv(command) {
|
|
|
84
108
|
}
|
|
85
109
|
if (char === '\\') {
|
|
86
110
|
const next = input[++index];
|
|
87
|
-
if (next === undefined
|
|
88
|
-
return
|
|
111
|
+
if (next === undefined)
|
|
112
|
+
return { ok: false, issue: 'invalid-quote' };
|
|
113
|
+
if (next === '\r' || next === '\n')
|
|
114
|
+
return { ok: false, issue: 'structural-newline' };
|
|
89
115
|
tokenStarted = true;
|
|
90
116
|
token += next;
|
|
91
117
|
continue;
|
|
@@ -94,14 +120,20 @@ export function parseLiteralShellArgv(command) {
|
|
|
94
120
|
// peer keys such as `aun#alice` are ordinary literal arguments.
|
|
95
121
|
if ((SHELL_UNQUOTED_META.has(char) && (char !== '#' || !tokenStarted))
|
|
96
122
|
|| (char === '~' && !tokenStarted))
|
|
97
|
-
return
|
|
123
|
+
return { ok: false, issue: 'shell-composition' };
|
|
98
124
|
tokenStarted = true;
|
|
99
125
|
token += char;
|
|
100
126
|
}
|
|
101
127
|
if (quote)
|
|
102
|
-
return
|
|
128
|
+
return { ok: false, issue: 'invalid-quote' };
|
|
103
129
|
finishToken();
|
|
104
|
-
return argv.length > 0
|
|
130
|
+
return argv.length > 0
|
|
131
|
+
? { ok: true, argv }
|
|
132
|
+
: { ok: false, issue: 'empty-command' };
|
|
133
|
+
}
|
|
134
|
+
export function parseLiteralShellArgv(command) {
|
|
135
|
+
const result = parseLiteralShellCommand(command);
|
|
136
|
+
return result.ok ? result.argv : null;
|
|
105
137
|
}
|
|
106
138
|
function scanBoundedOutputOperators(command) {
|
|
107
139
|
const operators = [];
|
|
@@ -118,12 +150,10 @@ function scanBoundedOutputOperators(command) {
|
|
|
118
150
|
quote = null;
|
|
119
151
|
continue;
|
|
120
152
|
}
|
|
121
|
-
if (
|
|
153
|
+
if (doubleQuotedIssueAt(command, index))
|
|
122
154
|
return null;
|
|
123
155
|
if (char === '\\') {
|
|
124
|
-
|
|
125
|
-
if (next === undefined || next === '\r' || next === '\n' || next === '$' || next === '`')
|
|
126
|
-
return null;
|
|
156
|
+
index++;
|
|
127
157
|
}
|
|
128
158
|
continue;
|
|
129
159
|
}
|
|
@@ -171,21 +201,46 @@ function scanBoundedOutputOperators(command) {
|
|
|
171
201
|
}
|
|
172
202
|
return quote ? null : operators;
|
|
173
203
|
}
|
|
174
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Accept `head` as a generic, trusted stdout filter without mirroring its
|
|
206
|
+
* option grammar here. Non-option operands are the security boundary: they
|
|
207
|
+
* could make `head` read a file instead of the EC process' stdout.
|
|
208
|
+
*
|
|
209
|
+
* GNU/POSIX head only uses a separate value for -n/-c (and their long forms).
|
|
210
|
+
* All other dash-prefixed arguments are safe to leave to head itself; unknown
|
|
211
|
+
* or malformed options simply make head fail.
|
|
212
|
+
*/
|
|
213
|
+
function parseStdinOnlyHeadArgs(segment) {
|
|
175
214
|
const argv = parseLiteralShellArgv(segment);
|
|
176
|
-
if (!argv || (argv[0]
|
|
177
|
-
return null;
|
|
178
|
-
let value;
|
|
179
|
-
if (argv.length === 1)
|
|
180
|
-
value = String(DEFAULT_HEAD_LINES);
|
|
181
|
-
else if (argv.length === 2 && /^-\d+$/.test(argv[1]))
|
|
182
|
-
value = argv[1].slice(1);
|
|
183
|
-
else if (argv.length === 3 && argv[1] === '-n' && /^\d+$/.test(argv[2]))
|
|
184
|
-
value = argv[2];
|
|
185
|
-
else
|
|
215
|
+
if (!argv || !HEAD_EXECUTABLES.has(argv[0]))
|
|
186
216
|
return null;
|
|
187
|
-
const
|
|
188
|
-
|
|
217
|
+
const args = argv.slice(1);
|
|
218
|
+
let optionsEnded = false;
|
|
219
|
+
let expectsOptionValue = false;
|
|
220
|
+
for (const arg of args) {
|
|
221
|
+
if (expectsOptionValue) {
|
|
222
|
+
expectsOptionValue = false;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (optionsEnded) {
|
|
226
|
+
if (arg !== '-')
|
|
227
|
+
return null;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (arg === '--') {
|
|
231
|
+
optionsEnded = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (arg === '-')
|
|
235
|
+
continue;
|
|
236
|
+
if (!arg.startsWith('-'))
|
|
237
|
+
return null;
|
|
238
|
+
if (arg === '-n' || arg === '-c' || arg === '--lines' || arg === '--bytes') {
|
|
239
|
+
expectsOptionValue = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return args;
|
|
189
244
|
}
|
|
190
245
|
/**
|
|
191
246
|
* Parse the only shell wrapper allowed around a privileged literal process:
|
|
@@ -234,13 +289,13 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
234
289
|
else {
|
|
235
290
|
redirect = first;
|
|
236
291
|
}
|
|
237
|
-
let
|
|
292
|
+
let headArgs;
|
|
238
293
|
if (pipe) {
|
|
239
294
|
const headEnd = redirect?.start ?? input.length;
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
295
|
+
const parsedArgs = parseStdinOnlyHeadArgs(trimShellSpacing(input.slice(pipe.end, headEnd)));
|
|
296
|
+
if (parsedArgs === null)
|
|
242
297
|
return null;
|
|
243
|
-
|
|
298
|
+
headArgs = parsedArgs;
|
|
244
299
|
}
|
|
245
300
|
let outputPath;
|
|
246
301
|
if (redirect) {
|
|
@@ -249,7 +304,7 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
249
304
|
return null;
|
|
250
305
|
outputPath = outputArgv[0];
|
|
251
306
|
}
|
|
252
|
-
return { argv, mergeStderr, ...(
|
|
307
|
+
return { argv, mergeStderr, ...(headArgs ? { headArgs } : {}), ...(outputPath ? { outputPath } : {}) };
|
|
253
308
|
}
|
|
254
309
|
/**
|
|
255
310
|
* Classify only the shell boundary around an EC process invocation. Runner
|
|
@@ -257,9 +312,9 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
257
312
|
* invocation may leave the sandbox, while shell composition may not.
|
|
258
313
|
*/
|
|
259
314
|
export function classifyEvolcoreShellCommand(command) {
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
262
|
-
return
|
|
315
|
+
const literal = parseLiteralShellCommand(command);
|
|
316
|
+
if (literal.ok) {
|
|
317
|
+
return literal.argv[0] === 'ec'
|
|
263
318
|
? { kind: 'literal' }
|
|
264
319
|
: { kind: 'none' };
|
|
265
320
|
}
|
|
@@ -268,7 +323,7 @@ export function classifyEvolcoreShellCommand(command) {
|
|
|
268
323
|
return { kind: 'bounded-output', command: boundedOutput };
|
|
269
324
|
}
|
|
270
325
|
return EVOLCORE_COMMAND_POSITION_RE.test(command)
|
|
271
|
-
? { kind: 'composite' }
|
|
326
|
+
? { kind: 'composite', issue: literal.issue }
|
|
272
327
|
: { kind: 'none' };
|
|
273
328
|
}
|
|
274
329
|
function isDeterministicFilePath(value) {
|
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
import { resolveRoot } from '../../paths.js';
|
|
6
|
-
import { containsHClassReference, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
|
|
6
|
+
import { containsHClassReference, containsLClassReference, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
|
|
7
7
|
import { classifyEvolcoreShellCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, } from './ec-command-parser.js';
|
|
8
8
|
// 绝对禁止命令(所有角色、所有权限模式下都禁止,不可授权)
|
|
9
9
|
// 这些是系统级破坏操作,任何情况下都不应该允许
|
|
@@ -18,6 +18,15 @@ const READONLY_SHELL_EXECUTABLES = new Set([
|
|
|
18
18
|
'ls', '/bin/ls', '/usr/bin/ls',
|
|
19
19
|
'cat', '/bin/cat', '/usr/bin/cat',
|
|
20
20
|
]);
|
|
21
|
+
const L_CLASS_READONLY_SHELL_EXECUTABLES = new Set([
|
|
22
|
+
...READONLY_SHELL_EXECUTABLES,
|
|
23
|
+
'grep', '/bin/grep', '/usr/bin/grep',
|
|
24
|
+
'rg', '/bin/rg', '/usr/bin/rg',
|
|
25
|
+
'find', '/bin/find', '/usr/bin/find',
|
|
26
|
+
'sed', '/bin/sed', '/usr/bin/sed',
|
|
27
|
+
]);
|
|
28
|
+
const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
|
|
29
|
+
const RG_PROCESS_SPAWNING_OPTION_RE = /^--pre(?:=|$)|^--pre-glob(?:=|$)/;
|
|
21
30
|
// 危险操作(需要用户审批才能执行)
|
|
22
31
|
// 这些操作有合理使用场景,但需要用户明确授权
|
|
23
32
|
const DANGEROUS_PATTERNS = [
|
|
@@ -347,25 +356,45 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
347
356
|
logger.warn(`[ReadonlyCheck] 🔒 Unknown tool blocked: tool=${toolName} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
348
357
|
return { behavior: 'deny', message: `🔒 只读模式:未声明为只读的工具 ${toolName} 已拒绝执行` };
|
|
349
358
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
359
|
+
const NON_FILESYSTEM_METADATA_TOOLS = new Set([
|
|
360
|
+
'Agent',
|
|
361
|
+
'AskUserQuestion',
|
|
362
|
+
'CronCreate',
|
|
363
|
+
'CronDelete',
|
|
364
|
+
'CronList',
|
|
365
|
+
'EnterPlanMode',
|
|
366
|
+
'ExitPlanMode',
|
|
367
|
+
'SendMessage',
|
|
368
|
+
'Skill',
|
|
369
|
+
'Task',
|
|
370
|
+
'TaskCreate',
|
|
371
|
+
'TaskGet',
|
|
372
|
+
'TaskList',
|
|
373
|
+
'TaskOutput',
|
|
374
|
+
'TaskStop',
|
|
375
|
+
'TaskUpdate',
|
|
376
|
+
'TeamCreate',
|
|
377
|
+
'TeamDelete',
|
|
378
|
+
'TodoWrite',
|
|
379
|
+
'ToolSearch',
|
|
380
|
+
'WebFetch',
|
|
381
|
+
'WebSearch',
|
|
382
|
+
]);
|
|
383
|
+
function collectOpaqueToolStrings(value, output) {
|
|
355
384
|
if (typeof value === 'string') {
|
|
356
|
-
|
|
385
|
+
if (value.trim())
|
|
386
|
+
output.push(value);
|
|
357
387
|
return;
|
|
358
388
|
}
|
|
359
389
|
if (Array.isArray(value)) {
|
|
360
390
|
for (const entry of value)
|
|
361
|
-
|
|
391
|
+
collectOpaqueToolStrings(entry, output);
|
|
362
392
|
return;
|
|
363
393
|
}
|
|
364
394
|
if (!value || typeof value !== 'object')
|
|
365
395
|
return;
|
|
366
|
-
for (const
|
|
367
|
-
output
|
|
368
|
-
collectStringValues(entry, output);
|
|
396
|
+
for (const entry of Object.values(value)) {
|
|
397
|
+
collectOpaqueToolStrings(entry, output);
|
|
369
398
|
}
|
|
370
399
|
}
|
|
371
400
|
function collectPermissionPathSpec(value, output, projectRootSubpaths) {
|
|
@@ -532,28 +561,50 @@ function collectFileChangePaths(value, output) {
|
|
|
532
561
|
}
|
|
533
562
|
function shellCommandReferencesLClass(command, options) {
|
|
534
563
|
const normalized = command.replace(/\\/g, '/');
|
|
535
|
-
|
|
536
|
-
const protectedRoot = `${root.replace(/\/+$/, '')}/data/triggers`;
|
|
537
|
-
if (normalized.includes(protectedRoot))
|
|
564
|
+
if (containsLClassReference(normalized))
|
|
538
565
|
return true;
|
|
539
|
-
const
|
|
540
|
-
return
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
566
|
+
const argv = parseLiteralShellArgv(command);
|
|
567
|
+
return argv?.slice(1).some(argument => {
|
|
568
|
+
if (argument === '-' || argument === '--' || argument.startsWith('-'))
|
|
569
|
+
return false;
|
|
570
|
+
return isLClassPath(argument, options);
|
|
571
|
+
}) ?? false;
|
|
544
572
|
}
|
|
545
|
-
function
|
|
573
|
+
function isVerifiedLClassReadOnlyShellCommand(command) {
|
|
574
|
+
const argv = parseLiteralShellArgv(command);
|
|
575
|
+
if (!argv || !L_CLASS_READONLY_SHELL_EXECUTABLES.has(argv[0]))
|
|
576
|
+
return false;
|
|
577
|
+
switch (argv[0].replace(/^.*\//, '')) {
|
|
578
|
+
case 'grep':
|
|
579
|
+
// grep never mutates its operands. Shell composition is rejected by the
|
|
580
|
+
// literal parser before this point.
|
|
581
|
+
return true;
|
|
582
|
+
case 'rg':
|
|
583
|
+
// ripgrep's --pre hooks may execute an arbitrary process.
|
|
584
|
+
return !argv.slice(1).some(argument => RG_PROCESS_SPAWNING_OPTION_RE.test(argument));
|
|
585
|
+
case 'find':
|
|
586
|
+
// Keep only the predicates/actions which cannot create, mutate, or
|
|
587
|
+
// execute files. Other find options remain fail-closed.
|
|
588
|
+
return !argv.slice(1).some(argument => FIND_MUTATING_PRIMARY_RE.test(argument));
|
|
589
|
+
case 'sed': {
|
|
590
|
+
// sed can write via -i or a `w` command. Permit only the bounded
|
|
591
|
+
// inspection form used for line-range reads: sed -n 'START[,END]p'.
|
|
592
|
+
const quiet = argv[1] === '-n' || argv[1] === '--quiet' || argv[1] === '--silent';
|
|
593
|
+
const program = quiet ? argv[2] : undefined;
|
|
594
|
+
return !!program
|
|
595
|
+
&& /^(?:\d+(?:,\d+)?|\$)?p$/.test(program)
|
|
596
|
+
// GNU sed accepts options after the script as well. Reject every
|
|
597
|
+
// later option so -i/-e/-f cannot be smuggled into this read form.
|
|
598
|
+
&& !argv.slice(3).some(argument => argument.startsWith('-'));
|
|
599
|
+
}
|
|
600
|
+
default:
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function isUnverifiedLClassShellCommand(command, options) {
|
|
546
605
|
if (!shellCommandReferencesLClass(command, options))
|
|
547
606
|
return false;
|
|
548
|
-
|
|
549
|
-
/(?:^|[;&|]\s*)\s*(?:rm|unlink|rmdir|mv|rename|truncate|touch|chmod|chown|chgrp|mkdir)\b/i,
|
|
550
|
-
/(?:^|[;&|]\s*)\s*(?:sed\s+[^;&|]*\s-i\b|perl\s+[^;&|]*\s-pi\b)/i,
|
|
551
|
-
/(?:^|[;&|]\s*)\s*tee\b/i,
|
|
552
|
-
/\bfind\b[^;&|]*(?:-delete|-exec\s+(?:rm|mv|chmod|chown)\b)/i,
|
|
553
|
-
/\bgit\s+(?:checkout|restore|clean|reset)\b/i,
|
|
554
|
-
/(?:^|[^<])>{1,2}\s*[^;&|\r\n]*data[\\/]triggers(?:[\\/]|$)/i,
|
|
555
|
-
];
|
|
556
|
-
return mutationPatterns.some(pattern => pattern.test(command));
|
|
607
|
+
return !isVerifiedLClassReadOnlyShellCommand(command);
|
|
557
608
|
}
|
|
558
609
|
function requestsFilesystemRoot(value) {
|
|
559
610
|
if (Array.isArray(value))
|
|
@@ -604,9 +655,18 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
604
655
|
collectFilesystemGrantPaths(input.additionalPermissions, grantPaths, projectRootGrantSubpaths);
|
|
605
656
|
collectFilesystemGrantPaths(input.permissions, grantPaths, projectRootGrantSubpaths);
|
|
606
657
|
}
|
|
607
|
-
else {
|
|
608
|
-
|
|
658
|
+
else if (!NON_FILESYSTEM_METADATA_TOOLS.has(toolName)) {
|
|
659
|
+
// Opaque external tools may run outside the Claude/Codex filesystem
|
|
660
|
+
// sandbox. Retain conservative string inspection for those tools, while
|
|
661
|
+
// excluding known orchestration tools whose payload is only metadata.
|
|
662
|
+
collectOpaqueToolStrings(input, paths);
|
|
609
663
|
}
|
|
664
|
+
// Agent/Skill/Task and other orchestration tools carry natural-language
|
|
665
|
+
// metadata, not direct filesystem operands. Treating every string in their
|
|
666
|
+
// payload as a path makes a prompt that merely mentions daemon.json or a
|
|
667
|
+
// *.json_ backup indistinguishable from an actual file access. Any nested
|
|
668
|
+
// filesystem operation is checked again when it reaches Read/Write/Bash,
|
|
669
|
+
// FileChange, or PermissionGrant above.
|
|
610
670
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
611
671
|
if (requestsFilesystemRoot(input.permissions) || requestsFilesystemRoot(input.additionalPermissions)) {
|
|
612
672
|
logger.warn(`[H-Class Protection] 🔒 Permission grant covers filesystem root: tool=${toolName} ` +
|
|
@@ -718,20 +778,23 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
718
778
|
else if (toolName === 'Bash') {
|
|
719
779
|
const command = typeof input.command === 'string' ? input.command.replace(/\\/g, '/') : '';
|
|
720
780
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
721
|
-
if (
|
|
722
|
-
logger.warn(`[L-Class Protection]
|
|
781
|
+
if (isUnverifiedLClassShellCommand(command, pathOptions)) {
|
|
782
|
+
logger.warn(`[L-Class Protection] Unverified L-class shell command denied: tool=${toolName} ` +
|
|
723
783
|
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
724
784
|
return {
|
|
725
785
|
behavior: 'deny',
|
|
726
|
-
message: '🔒
|
|
786
|
+
message: '🔒 L-class 路径仅允许单个可验证的只读命令。请拆分为单条 ls/cat/grep/rg/find 或 sed -n,或使用 Read;禁止管道、重定向、变量展开和命令拼接。修改必须通过受控的 EvolCore 命令',
|
|
727
787
|
};
|
|
728
788
|
}
|
|
729
789
|
collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
730
790
|
collectFilesystemWriteGrantPaths(input.permissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
731
791
|
}
|
|
732
|
-
else {
|
|
733
|
-
|
|
792
|
+
else if (!NON_FILESYSTEM_METADATA_TOOLS.has(toolName)) {
|
|
793
|
+
collectOpaqueToolStrings(input, paths);
|
|
734
794
|
}
|
|
795
|
+
// Do not infer mutation targets from arbitrary orchestration-tool strings.
|
|
796
|
+
// Skills commonly pass args: "" and delegation prompts may discuss
|
|
797
|
+
// protected paths; neither is itself a filesystem mutation.
|
|
735
798
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
736
799
|
for (const grantPath of writeGrantPaths) {
|
|
737
800
|
if (grantPath && lClassGrantIncludesProtectedPath(grantPath, pathOptions)) {
|
|
@@ -759,7 +822,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
759
822
|
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
760
823
|
return {
|
|
761
824
|
behavior: 'deny',
|
|
762
|
-
message: `🔒 L-class 路径允许读取但禁止 Agent 直接修改、删除或移动\n\n文件:${filePath}\n\n💡
|
|
825
|
+
message: `🔒 L-class 路径允许读取但禁止 Agent 直接修改、删除或移动\n\n文件:${filePath}\n\n💡 请使用对应的 EvolCore 受控命令修改`,
|
|
763
826
|
};
|
|
764
827
|
}
|
|
765
828
|
return { behavior: 'allow' };
|
|
@@ -862,7 +925,7 @@ function prepareBoundedOutputInput(input, command, kind, context) {
|
|
|
862
925
|
cwd: path.resolve(context.projectPath),
|
|
863
926
|
argv: command.argv,
|
|
864
927
|
mergeStderr: command.mergeStderr,
|
|
865
|
-
...(command.
|
|
928
|
+
...(command.headArgs ? { headArgs: command.headArgs } : {}),
|
|
866
929
|
...(command.outputPath ? { outputPath: command.outputPath } : {}),
|
|
867
930
|
}), 'utf8').toString('base64url');
|
|
868
931
|
const rewrittenCommand = `${quotePosixShellArg(process.execPath)} ${quotePosixShellArg(helperPath)} ${quotePosixShellArg(payload)}`;
|
|
@@ -884,6 +947,20 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
884
947
|
return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
|
|
885
948
|
}
|
|
886
949
|
if (ecCommand.kind === 'composite') {
|
|
950
|
+
if (ecCommand.issue === 'unsafe-expansion') {
|
|
951
|
+
return {
|
|
952
|
+
behavior: 'deny',
|
|
953
|
+
input,
|
|
954
|
+
message: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量',
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
if (ecCommand.issue === 'invalid-quote') {
|
|
958
|
+
return {
|
|
959
|
+
behavior: 'deny',
|
|
960
|
+
input,
|
|
961
|
+
message: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"',
|
|
962
|
+
};
|
|
963
|
+
}
|
|
887
964
|
return {
|
|
888
965
|
behavior: 'deny',
|
|
889
966
|
input,
|