evolcore 0.0.21 → 0.0.23
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/AGENTS.md +62 -0
- package/CHANGELOG.md +53 -0
- package/README.md +2 -0
- package/bin/codex-managed-acl.ps1 +155 -0
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +376 -96
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +1 -1
- package/dist/agents/claude-runner.js +44 -226
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +154 -0
- package/dist/agents/codex-runner.js +1292 -219
- package/dist/agents/ecagent-runner.js +10 -31
- package/dist/agents/gemini-runner.js +2 -6
- package/dist/aun/aid/store.js +2 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +51 -4
- package/dist/aun/outbox.js +7 -0
- package/dist/aun/rpc/client.js +8 -1
- package/dist/channels/aun.js +240 -36
- package/dist/channels/wechat.js +81 -10
- package/dist/cli/aun-commands.js +48 -4
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/daemon-commands.js +169 -51
- package/dist/cli/index.js +20 -0
- package/dist/cli/init.js +2 -10
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/task-context.js +16 -2
- package/dist/cli/trigger-command.js +22 -5
- package/dist/config/config-field-policy.js +3 -0
- package/dist/config/config-manager.js +32 -5
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +15 -0
- package/dist/core/auth/auth-gateway.js +8 -0
- package/dist/core/auth/authorization-audit.js +70 -6
- package/dist/core/auth/operation-catalog.js +4 -0
- package/dist/core/auth/remote-access-store.js +135 -0
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +93 -25
- package/dist/core/command/command-handler.js +62 -5
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +150 -33
- package/dist/core/command/menu-protocol.js +120 -1
- package/dist/core/command/slash-gate.js +3 -3
- package/dist/core/command/slash-handler.js +83 -40
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +32 -0
- package/dist/core/handoff/runtime.js +23 -3
- package/dist/core/message/im-renderer.js +7 -3
- package/dist/core/message/message-bridge.js +60 -2
- package/dist/core/message/message-log.js +33 -0
- package/dist/core/message/message-queue.js +41 -15
- package/dist/core/message/response-engine.js +246 -114
- package/dist/core/model/catalog-service.js +308 -0
- package/dist/core/model/model-catalog.js +124 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +1 -1
- package/dist/core/permission/approval-gateway.js +16 -1
- package/dist/core/permission/ec-command-parser.js +272 -70
- package/dist/core/permission/protected-paths.js +74 -14
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +290 -30
- package/dist/core/runtime-lock.js +13 -8
- package/dist/core/session/session-manager.js +30 -0
- package/dist/core/session/session-renew.js +18 -1
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/index.js +104 -34
- package/dist/ipc.js +218 -96
- package/dist/paths.js +18 -0
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/feedback.js +14 -2
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +86 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/trigger/scheduler.js +20 -3
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/logger.js +9 -4
- package/dist/utils/npm-ops.js +47 -15
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/evolcore/INDEX.md +3 -2
- package/kits/docs/evolcore/agent-create.md +159 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/group-collaboration.md +256 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/schemas/_meta.json +3 -2
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +10 -2
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +11 -3
|
@@ -268,21 +268,21 @@ export function getHClassSandboxPatterns(root = resolveRoot()) {
|
|
|
268
268
|
'.lock',
|
|
269
269
|
'daemon.pid',
|
|
270
270
|
path.join('data', 'causation-aun.json*'),
|
|
271
|
-
path.join('data', 'triggers'
|
|
272
|
-
path.join('data', 'sessions'
|
|
271
|
+
path.join('data', 'triggers') + '/**',
|
|
272
|
+
path.join('data', 'sessions') + '/**',
|
|
273
273
|
path.join('data', 'contact-book-audit.jsonl'),
|
|
274
274
|
path.join('data', 'feishu-seen-*.jsonl'),
|
|
275
275
|
path.join('data', '*-greeted-*.jsonl'),
|
|
276
276
|
path.join('data', 'message-queue.json*'),
|
|
277
277
|
path.join('data', 'daemon', 'message-queue.json*'),
|
|
278
|
-
path.join('data', 'daemon', 'control'
|
|
279
|
-
path.join('data', 'daemon', 'migrations'
|
|
280
|
-
path.join('data', 'channel-state'
|
|
278
|
+
path.join('data', 'daemon', 'control') + '/**',
|
|
279
|
+
path.join('data', 'daemon', 'migrations') + '/**',
|
|
280
|
+
path.join('data', 'channel-state') + '/**',
|
|
281
281
|
path.join('data', 'instance', 'control.token'),
|
|
282
282
|
path.join('data', 'outbox'),
|
|
283
|
-
path.join('data', 'outbox'
|
|
283
|
+
path.join('data', 'outbox') + '/**',
|
|
284
284
|
path.join('data', 'handoff'),
|
|
285
|
-
path.join('data', 'handoff'
|
|
285
|
+
path.join('data', 'handoff') + '/**',
|
|
286
286
|
path.join('agents', '*', 'sessions', '**'),
|
|
287
287
|
path.join('agents', '*', 'data', 'channels', '**'),
|
|
288
288
|
path.join('agents', '*', 'data', 'handoff', '**'),
|
|
@@ -296,12 +296,13 @@ export function getLClassWritePatterns(root = resolveRoot()) {
|
|
|
296
296
|
const absoluteRoot = resolveThroughExistingAncestor(root);
|
|
297
297
|
return [
|
|
298
298
|
path.join(absoluteRoot, 'agents', '*', 'triggers'),
|
|
299
|
-
path.join(absoluteRoot, 'agents', '*', 'triggers'
|
|
300
|
-
path.join(absoluteRoot, 'agents', '*', 'roles'
|
|
299
|
+
path.join(absoluteRoot, 'agents', '*', 'triggers') + '/**',
|
|
300
|
+
path.join(absoluteRoot, 'agents', '*', 'roles') + '/**',
|
|
301
301
|
];
|
|
302
302
|
}
|
|
303
303
|
const CLAUDE_PROTECTED_TARGET_LIMIT = 8192;
|
|
304
304
|
const CLAUDE_GLOB_MAGIC = /[*?[\]]/;
|
|
305
|
+
const CODEX_GLOB_MAGIC = /[*?[\]]/;
|
|
305
306
|
function addExistingTarget(targets, value) {
|
|
306
307
|
try {
|
|
307
308
|
const stat = fs.lstatSync(value);
|
|
@@ -427,12 +428,70 @@ export function buildClaudeProtectedFilesystem(root = resolveRoot(), options = {
|
|
|
427
428
|
};
|
|
428
429
|
}
|
|
429
430
|
export function buildCodexHClassFilesystemRules(root = resolveRoot()) {
|
|
430
|
-
|
|
431
|
+
const filesystem = {
|
|
431
432
|
// Codex pre-expands unbounded deny globs on Linux/WSL/Windows. Without a
|
|
432
433
|
// bound, patterns such as **/*.json_ are accepted but may not be enforced.
|
|
433
434
|
glob_scan_max_depth: 64,
|
|
434
|
-
...Object.fromEntries(getHClassSandboxPatterns(root).map(pattern => [pattern, 'deny'])),
|
|
435
|
+
...Object.fromEntries(filterWindowsCodexFilesystemTargets(getHClassSandboxPatterns(root)).map(pattern => [pattern, 'deny'])),
|
|
435
436
|
};
|
|
437
|
+
return compactWindowsCodexFilesystemRules(filesystem);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Windows Codex sandbox setup materializes a path while applying its deny-read
|
|
441
|
+
* ACL. A nonexistent exact file target can therefore become a directory on
|
|
442
|
+
* disk (for example `daemon.pid`). Keep existing paths and globs, but omit
|
|
443
|
+
* nonexistent exact paths so the sandbox never creates the wrong filesystem
|
|
444
|
+
* object. Non-Windows callers retain the historical rule set.
|
|
445
|
+
*/
|
|
446
|
+
export function filterWindowsCodexFilesystemTargets(patterns, platform = process.platform) {
|
|
447
|
+
if (platform !== 'win32')
|
|
448
|
+
return patterns;
|
|
449
|
+
return patterns.filter(pattern => {
|
|
450
|
+
if (CODEX_GLOB_MAGIC.test(pattern))
|
|
451
|
+
return true;
|
|
452
|
+
try {
|
|
453
|
+
fs.lstatSync(pattern);
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
const code = error && typeof error === 'object' && 'code' in error
|
|
458
|
+
? error.code
|
|
459
|
+
: undefined;
|
|
460
|
+
return code === 'ENOENT' ? false : true;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Codex expands recursive deny globs into individual targets. Large protected
|
|
466
|
+
* trees on Windows can then fail during AGENTS.md loading with os error 206.
|
|
467
|
+
* Directory denies inherit to descendants through the Windows sandbox ACLs,
|
|
468
|
+
* avoiding redundant targets. Keep broad recursive filename globs intact.
|
|
469
|
+
*/
|
|
470
|
+
export function compactWindowsCodexFilesystemRules(filesystem, platform = process.platform) {
|
|
471
|
+
if (platform !== 'win32')
|
|
472
|
+
return filesystem;
|
|
473
|
+
const compacted = {};
|
|
474
|
+
for (const [pattern, access] of Object.entries(filesystem)) {
|
|
475
|
+
const isTerminalRecursiveDeny = access === 'deny'
|
|
476
|
+
&& (pattern.endsWith('/**') || pattern.endsWith('\\**'));
|
|
477
|
+
const recursiveRoot = isTerminalRecursiveDeny ? pattern.slice(0, -3) : undefined;
|
|
478
|
+
// Only collapse a recursive deny when its root is a real directory. A
|
|
479
|
+
// missing root must remain a glob: turning it into an exact target can
|
|
480
|
+
// make the Windows sandbox materialize that path as a directory.
|
|
481
|
+
const canCompact = !!recursiveRoot
|
|
482
|
+
&& (CODEX_GLOB_MAGIC.test(recursiveRoot) || (() => {
|
|
483
|
+
try {
|
|
484
|
+
return fs.lstatSync(recursiveRoot).isDirectory();
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
})());
|
|
490
|
+
const compactPattern = canCompact ? recursiveRoot : pattern;
|
|
491
|
+
if (compacted[compactPattern] !== 'deny')
|
|
492
|
+
compacted[compactPattern] = access;
|
|
493
|
+
}
|
|
494
|
+
return compacted;
|
|
436
495
|
}
|
|
437
496
|
export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options = {}) {
|
|
438
497
|
const lClassRules = {};
|
|
@@ -451,17 +510,18 @@ export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options
|
|
|
451
510
|
lClassRules[target.path] = lClassAccess;
|
|
452
511
|
}
|
|
453
512
|
if (expandLClassReadDeny || !options.denyLClassRead) {
|
|
454
|
-
|
|
513
|
+
// Use '/**' suffix instead of path.join to satisfy Codex's glob requirement
|
|
514
|
+
lClassRules[target.path + '/**'] = lClassAccess;
|
|
455
515
|
}
|
|
456
516
|
}
|
|
457
517
|
else {
|
|
458
518
|
lClassRules[target.path] = lClassAccess;
|
|
459
519
|
}
|
|
460
520
|
}
|
|
461
|
-
return {
|
|
521
|
+
return compactWindowsCodexFilesystemRules({
|
|
462
522
|
...buildCodexHClassFilesystemRules(root),
|
|
463
523
|
...lClassRules,
|
|
464
|
-
};
|
|
524
|
+
});
|
|
465
525
|
}
|
|
466
526
|
export function hClassGrantIncludesProtectedPath(value, options = {}) {
|
|
467
527
|
const root = resolveThroughExistingAncestor(options.root ?? resolveRoot());
|
|
@@ -497,8 +497,10 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
|
|
|
497
497
|
'--dev-bind', '/dev', '/dev',
|
|
498
498
|
'--proc', '/proc',
|
|
499
499
|
];
|
|
500
|
-
|
|
501
|
-
|
|
500
|
+
if (options.protectEvolcoreFilesystem !== false) {
|
|
501
|
+
appendLClassReadOnlyBinds(args, path.resolve(root));
|
|
502
|
+
appendHClassMasks(args, path.resolve(root), true);
|
|
503
|
+
}
|
|
502
504
|
if (options.managedCodexRequirements) {
|
|
503
505
|
const requirements = path.resolve(options.managedCodexRequirements);
|
|
504
506
|
if (!fs.statSync(requirements).isFile()) {
|
|
@@ -14,6 +14,18 @@ export function classifyToolErrorCode(input) {
|
|
|
14
14
|
const explicit = normalizeToolErrorCode(input.errorCode);
|
|
15
15
|
if (explicit)
|
|
16
16
|
return explicit;
|
|
17
|
+
if (input.decisionSource === 'policy')
|
|
18
|
+
return 'POLICY_DENIED';
|
|
19
|
+
if (input.decisionSource === 'approval')
|
|
20
|
+
return 'USER_DENIED';
|
|
21
|
+
return 'EXECUTION_FAILED';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compatibility classifier for historical, unstructured logs only.
|
|
25
|
+
* Production event producers must use classifyToolErrorCode() and explicit
|
|
26
|
+
* errorCode/decisionSource fields instead of interpreting command output.
|
|
27
|
+
*/
|
|
28
|
+
export function classifyLegacyToolErrorText(input) {
|
|
17
29
|
const text = [input.error, input.result]
|
|
18
30
|
.map(value => {
|
|
19
31
|
if (typeof value === 'string')
|
|
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto';
|
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
import { resolveRoot } from '../../paths.js';
|
|
6
6
|
import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from './protected-paths.js';
|
|
7
|
-
import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent,
|
|
7
|
+
import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, hasPowerShellDelegationAssignment, parseCodexToolCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
|
|
8
8
|
import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
|
|
9
9
|
/** Resolve the session-owned temporary root supplied by the execution host. */
|
|
10
10
|
export function resolveManagedTempDir(env = process.env) {
|
|
@@ -91,15 +91,179 @@ export function maskManagedTempShellPaths(command, options = {}) {
|
|
|
91
91
|
return `__managed_tmp_${index}__`;
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
-
// 绝对禁止命令(所有角色、所有权限模式下都禁止,不可授权)
|
|
95
|
-
// 这些是系统级破坏操作,任何情况下都不应该允许
|
|
96
94
|
const ABSOLUTE_FORBIDDEN = [
|
|
97
|
-
/\bshutdown\b
|
|
98
|
-
/\breboot\b
|
|
99
|
-
/\bmkfs
|
|
100
|
-
/\bformat\s+[a-zA-Z]:/i,
|
|
101
|
-
/\bdd\s+if=.*of=\/dev
|
|
95
|
+
/\bshutdown\b/i,
|
|
96
|
+
/\breboot\b/i,
|
|
97
|
+
/\bmkfs(?:\.[A-Za-z0-9._-]+)?\b/i,
|
|
98
|
+
/\bformat\s+[a-zA-Z]:/i,
|
|
99
|
+
/\bdd\s+if=.*of=\/dev(?:\/|\b)/i,
|
|
102
100
|
];
|
|
101
|
+
const SCRIPT_EXECUTION_RE = /(?:\bos\.system\s*\(|\bsubprocess\.(?:run|call|Popen)\s*\(|\bchild_process\.(?:exec|spawn|fork)\s*\(|\b(?:system|exec|spawn|popen)\s*\()/i;
|
|
102
|
+
function hereDocDelimiter(line) {
|
|
103
|
+
let quote;
|
|
104
|
+
let escaped = false;
|
|
105
|
+
for (let index = 0; index < line.length - 1; index++) {
|
|
106
|
+
const char = line[index];
|
|
107
|
+
if (escaped) {
|
|
108
|
+
escaped = false;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (char === '\\' && quote !== 'single') {
|
|
112
|
+
escaped = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (quote === 'single') {
|
|
116
|
+
if (char === "'")
|
|
117
|
+
quote = undefined;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (quote === 'double') {
|
|
121
|
+
if (char === '"')
|
|
122
|
+
quote = undefined;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (char === "'") {
|
|
126
|
+
quote = 'single';
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (char === '"') {
|
|
130
|
+
quote = 'double';
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (char !== '<' || line[index + 1] !== '<' || line[index + 2] === '<')
|
|
134
|
+
continue;
|
|
135
|
+
let cursor = index + 2;
|
|
136
|
+
if (line[cursor] === '-')
|
|
137
|
+
cursor++;
|
|
138
|
+
while (cursor < line.length && /\s/.test(line[cursor]))
|
|
139
|
+
cursor++;
|
|
140
|
+
if (line[cursor] === "'" || line[cursor] === '"') {
|
|
141
|
+
const delimiterQuote = line[cursor++];
|
|
142
|
+
const start = cursor;
|
|
143
|
+
while (cursor < line.length && line[cursor] !== delimiterQuote)
|
|
144
|
+
cursor++;
|
|
145
|
+
return cursor > start && cursor < line.length ? line.slice(start, cursor) : undefined;
|
|
146
|
+
}
|
|
147
|
+
const match = line.slice(cursor).match(/^[A-Za-z_][A-Za-z0-9_-]*/);
|
|
148
|
+
return match?.[0];
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
function splitHereDocBodies(command) {
|
|
153
|
+
const lines = command.split(/\r?\n/);
|
|
154
|
+
let delimiter;
|
|
155
|
+
let body;
|
|
156
|
+
const bodies = [];
|
|
157
|
+
const retained = [];
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
if (delimiter) {
|
|
160
|
+
const candidate = line.replace(/^\t+/, '').trim();
|
|
161
|
+
if (candidate === delimiter) {
|
|
162
|
+
bodies.push((body ?? []).join('\n'));
|
|
163
|
+
delimiter = undefined;
|
|
164
|
+
body = undefined;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
body?.push(line);
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
retained.push(line);
|
|
172
|
+
delimiter = hereDocDelimiter(line);
|
|
173
|
+
if (delimiter)
|
|
174
|
+
body = [];
|
|
175
|
+
}
|
|
176
|
+
if (delimiter && body)
|
|
177
|
+
bodies.push(body.join('\n'));
|
|
178
|
+
return { command: retained.join('\n'), bodies };
|
|
179
|
+
}
|
|
180
|
+
function containsAbsoluteForbiddenText(value) {
|
|
181
|
+
return ABSOLUTE_FORBIDDEN.some(pattern => pattern.test(value));
|
|
182
|
+
}
|
|
183
|
+
function interpreterScript(args) {
|
|
184
|
+
const index = args.findIndex(argument => argument === '-c' || argument === '-e' || argument === '--eval');
|
|
185
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
186
|
+
}
|
|
187
|
+
function scriptContainsAbsoluteForbidden(script) {
|
|
188
|
+
return SCRIPT_EXECUTION_RE.test(script) && containsAbsoluteForbiddenText(script);
|
|
189
|
+
}
|
|
190
|
+
/** Match only executable argv, never regex/search text or script literals. */
|
|
191
|
+
function containsAbsoluteForbiddenCommand(command, depth = 0) {
|
|
192
|
+
const hereDocs = splitHereDocBodies(command);
|
|
193
|
+
if (containsShellSubstitution(hereDocs.command) && containsAbsoluteForbiddenText(hereDocs.command))
|
|
194
|
+
return true;
|
|
195
|
+
return splitShellCommandSegments(hereDocs.command).some(segment => {
|
|
196
|
+
const parsed = parseEffectiveCommand(segment);
|
|
197
|
+
if (!parsed)
|
|
198
|
+
return false;
|
|
199
|
+
const { executable, args } = parsed;
|
|
200
|
+
if (executable === 'shutdown' || executable === 'reboot')
|
|
201
|
+
return true;
|
|
202
|
+
if (/^mkfs(?:\..*)?$/.test(executable))
|
|
203
|
+
return true;
|
|
204
|
+
if (executable === 'format' && args.some(argument => /^[A-Za-z]:$/.test(argument)))
|
|
205
|
+
return true;
|
|
206
|
+
if (executable === 'dd'
|
|
207
|
+
&& args.some(argument => /^if=/.test(argument))
|
|
208
|
+
&& args.some(argument => /^of=\/dev(?:\/|$)/.test(argument)))
|
|
209
|
+
return true;
|
|
210
|
+
if (executable === 'eval' && containsAbsoluteForbiddenText(args.join(' ')))
|
|
211
|
+
return true;
|
|
212
|
+
if (['node', 'nodejs', 'python', 'python3', 'perl'].includes(executable)) {
|
|
213
|
+
const script = interpreterScript(args);
|
|
214
|
+
if (script && scriptContainsAbsoluteForbidden(script))
|
|
215
|
+
return true;
|
|
216
|
+
if (hereDocs.bodies.some(scriptContainsAbsoluteForbidden))
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (['bash', 'sh', 'zsh', 'dash'].includes(executable)
|
|
220
|
+
&& hereDocs.bodies.some(body => containsAbsoluteForbiddenCommand(body, depth + 1)))
|
|
221
|
+
return true;
|
|
222
|
+
if (depth >= 2 || !['bash', 'sh', 'zsh', 'dash', 'cmd', 'cmd.exe'].includes(executable))
|
|
223
|
+
return false;
|
|
224
|
+
const commandFlag = executable.startsWith('cmd') ? '/c' : '-c';
|
|
225
|
+
const commandIndex = args.findIndex(argument => argument.toLowerCase() === commandFlag);
|
|
226
|
+
return commandIndex >= 0
|
|
227
|
+
&& !!args[commandIndex + 1]
|
|
228
|
+
&& containsAbsoluteForbiddenCommand(args.slice(commandIndex + 1).join(' '), depth + 1);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function containsShellSubstitution(command) {
|
|
232
|
+
let quote;
|
|
233
|
+
let escaped = false;
|
|
234
|
+
for (let index = 0; index < command.length; index++) {
|
|
235
|
+
const char = command[index];
|
|
236
|
+
if (escaped) {
|
|
237
|
+
escaped = false;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (char === '\\' && quote !== 'single') {
|
|
241
|
+
escaped = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (quote === 'single') {
|
|
245
|
+
if (char === "'")
|
|
246
|
+
quote = undefined;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (quote === 'double') {
|
|
250
|
+
if (char === '"')
|
|
251
|
+
quote = undefined;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (char === "'") {
|
|
255
|
+
quote = 'single';
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (char === '"') {
|
|
259
|
+
quote = 'double';
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (char === '`' || (char === '$' && command[index + 1] === '('))
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
103
267
|
// 危险操作(需要用户审批才能执行)
|
|
104
268
|
// 这些操作有合理使用场景,但需要用户明确授权
|
|
105
269
|
const DANGEROUS_PATTERNS = [
|
|
@@ -386,7 +550,8 @@ function classifyStructuredCommand(command, depth = 0) {
|
|
|
386
550
|
return matches.find(match => match.approvalScope === 'all') ?? matches[0];
|
|
387
551
|
}
|
|
388
552
|
function containsNetStopCommand(command, depth = 0) {
|
|
389
|
-
|
|
553
|
+
const hereDocs = splitHereDocBodies(command);
|
|
554
|
+
return splitShellCommandSegments(hereDocs.command).some(segment => {
|
|
390
555
|
const parsed = parseEffectiveCommand(segment);
|
|
391
556
|
if (!parsed)
|
|
392
557
|
return false;
|
|
@@ -394,6 +559,9 @@ function containsNetStopCommand(command, depth = 0) {
|
|
|
394
559
|
&& parsed.args[0]?.toLowerCase() === 'stop') {
|
|
395
560
|
return true;
|
|
396
561
|
}
|
|
562
|
+
if (['bash', 'sh', 'zsh', 'dash'].includes(parsed.executable)
|
|
563
|
+
&& hereDocs.bodies.some(body => containsNetStopCommand(body, depth + 1)))
|
|
564
|
+
return true;
|
|
397
565
|
if (depth >= 2 || !['bash', 'sh', 'zsh', 'dash', 'cmd', 'cmd.exe'].includes(parsed.executable)) {
|
|
398
566
|
return false;
|
|
399
567
|
}
|
|
@@ -1192,6 +1360,54 @@ function readonlySourceDiagnosticAllowed(analysis, projectPath) {
|
|
|
1192
1360
|
&& !isLClassPath(candidate, { root: resolveRoot() });
|
|
1193
1361
|
});
|
|
1194
1362
|
}
|
|
1363
|
+
/**
|
|
1364
|
+
* A narrow exception for the common repository source inventory query. The
|
|
1365
|
+
* general recursive H-class rule remains fail-closed; only an opt-in,
|
|
1366
|
+
* single-find query whose output is limited to TypeScript/JavaScript files at
|
|
1367
|
+
* the project root can use this path.
|
|
1368
|
+
*/
|
|
1369
|
+
function boundedRootSourceEnumeration(analysis, projectPath) {
|
|
1370
|
+
if (analysis.kind !== 'proven-readonly' || analysis.ir.pipelines.length !== 1)
|
|
1371
|
+
return false;
|
|
1372
|
+
const pipeline = analysis.ir.pipelines[0];
|
|
1373
|
+
const find = pipeline[0];
|
|
1374
|
+
if (!find || find.executable !== 'find' || find.pathOperands.length !== 1 || find.pathOperands[0].access !== 'recursive') {
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
const workspace = resolveProtectedCandidate(projectPath);
|
|
1378
|
+
if (workspace === path.parse(workspace).root)
|
|
1379
|
+
return false;
|
|
1380
|
+
const scope = resolveProtectedCandidate(find.pathOperands[0].value, projectPath);
|
|
1381
|
+
if (scope !== workspace)
|
|
1382
|
+
return false;
|
|
1383
|
+
const allowedPredicates = new Set(['-o', '-or', '-a', '-and', '-print', '-print0']);
|
|
1384
|
+
let sawSourceSuffix = false;
|
|
1385
|
+
const scopeIndex = find.argv.indexOf(find.pathOperands[0].value);
|
|
1386
|
+
if (scopeIndex < 1)
|
|
1387
|
+
return false;
|
|
1388
|
+
const args = find.argv.slice(scopeIndex + 1);
|
|
1389
|
+
for (let index = 0; index < args.length; index++) {
|
|
1390
|
+
const argument = args[index];
|
|
1391
|
+
if (argument === '-name' || argument === '-iname') {
|
|
1392
|
+
const pattern = args[++index];
|
|
1393
|
+
if (!pattern || !/^\*\.(?:ts|js)$/i.test(pattern))
|
|
1394
|
+
return false;
|
|
1395
|
+
sawSourceSuffix = true;
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (argument === '-type') {
|
|
1399
|
+
if (args[++index] !== 'f')
|
|
1400
|
+
return false;
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
if (!allowedPredicates.has(argument))
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
if (!sawSourceSuffix)
|
|
1407
|
+
return false;
|
|
1408
|
+
return pipeline.slice(1).every(command => ['head', 'sort', 'grep', 'wc'].includes(command.executable)
|
|
1409
|
+
&& command.pathOperands.length === 0);
|
|
1410
|
+
}
|
|
1195
1411
|
/** A lock filename used as a query pattern is safe; an operand naming the
|
|
1196
1412
|
* actual lock path is still protected. */
|
|
1197
1413
|
function containsLockPathReference(value) {
|
|
@@ -1246,10 +1462,13 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
1246
1462
|
managedTempDir,
|
|
1247
1463
|
allowProtectedMetadata: context?.allowProtectedMetadata,
|
|
1248
1464
|
});
|
|
1465
|
+
const sourceQueryAllowed = readonlySourceDiagnosticAllowed(protectedOperands.analysis, projectPath);
|
|
1466
|
+
const boundedSourceQueryAllowed = boundedRootSourceEnumeration(protectedOperands.analysis, projectPath);
|
|
1249
1467
|
const sourceDiagnosticAllowed = context?.allowReadonlySourceDiagnostics
|
|
1250
|
-
&&
|
|
1251
|
-
&&
|
|
1252
|
-
|
|
1468
|
+
&& (!protectedOperands.lClass || boundedSourceQueryAllowed)
|
|
1469
|
+
&& ((sourceQueryAllowed
|
|
1470
|
+
&& (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass)))
|
|
1471
|
+
|| boundedSourceQueryAllowed);
|
|
1253
1472
|
if (sourceDiagnosticAllowed) {
|
|
1254
1473
|
return { behavior: 'allow' };
|
|
1255
1474
|
}
|
|
@@ -1556,11 +1775,15 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1556
1775
|
managedTempDir: context?.managedTempDir,
|
|
1557
1776
|
allowProtectedMetadata: context?.allowProtectedMetadata,
|
|
1558
1777
|
});
|
|
1778
|
+
const sourceDiagnosticProjectPath = context?.projectPath ?? process.cwd();
|
|
1779
|
+
const sourceQueryAllowed = readonlySourceDiagnosticAllowed(protectedOperands.analysis, sourceDiagnosticProjectPath);
|
|
1780
|
+
const boundedSourceQueryAllowed = boundedRootSourceEnumeration(protectedOperands.analysis, sourceDiagnosticProjectPath);
|
|
1559
1781
|
const sourceDiagnosticAllowed = context?.permissionMode === 'readonly'
|
|
1560
1782
|
&& context.allowReadonlySourceDiagnostics === true
|
|
1561
|
-
&&
|
|
1562
|
-
&&
|
|
1563
|
-
|
|
1783
|
+
&& (!protectedOperands.lClass || boundedSourceQueryAllowed)
|
|
1784
|
+
&& ((sourceQueryAllowed
|
|
1785
|
+
&& (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass)))
|
|
1786
|
+
|| boundedSourceQueryAllowed);
|
|
1564
1787
|
if (protectedOperands.hClass && !sourceDiagnosticAllowed) {
|
|
1565
1788
|
logger.warn(`[H-Class Protection] 🔒 Protected filesystem operand in shell command: tool=${toolName} ` +
|
|
1566
1789
|
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
@@ -1721,7 +1944,10 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1721
1944
|
...pathOptions,
|
|
1722
1945
|
managedTempDir: context?.managedTempDir,
|
|
1723
1946
|
});
|
|
1947
|
+
const boundedSourceQueryAllowed = context?.allowReadonlySourceDiagnostics === true
|
|
1948
|
+
&& boundedRootSourceEnumeration(protectedOperands.analysis, context?.projectPath ?? process.cwd());
|
|
1724
1949
|
const lClassDenied = protectedOperands.lClass
|
|
1950
|
+
&& !boundedSourceQueryAllowed
|
|
1725
1951
|
&& (context?.permissionMode === 'readonly' || protectedOperands.analysis.kind === 'unproven');
|
|
1726
1952
|
if (lClassDenied) {
|
|
1727
1953
|
logger.warn(`[L-Class Protection] L-class shell access denied: tool=${toolName} ` +
|
|
@@ -1789,23 +2015,15 @@ export function checkBlacklist(toolName, input) {
|
|
|
1789
2015
|
if (!cmd || cmd.trim() === '') {
|
|
1790
2016
|
return { behavior: 'allow', updatedInput: input };
|
|
1791
2017
|
}
|
|
1792
|
-
|
|
2018
|
+
// 只根据解析后的 executable 和 argv 判断,避免把搜索词、正则和
|
|
2019
|
+
// heredoc/script 正文当成实际命令。
|
|
2020
|
+
if (containsNetStopCommand(cmd) || containsAbsoluteForbiddenCommand(cmd)) {
|
|
1793
2021
|
const cmdPreview = cmd.length > 60 ? cmd.substring(0, 60) + '...' : cmd;
|
|
1794
2022
|
return {
|
|
1795
2023
|
behavior: 'deny',
|
|
1796
2024
|
message: `🚫 系统级危险操作已拦截\n命令:${cmdPreview}\n\n此类操作可能导致系统损坏,任何权限模式下都不允许执行。`,
|
|
1797
2025
|
};
|
|
1798
2026
|
}
|
|
1799
|
-
// 检查绝对禁止命令(系统级破坏操作)
|
|
1800
|
-
for (const pattern of ABSOLUTE_FORBIDDEN) {
|
|
1801
|
-
if (pattern.test(cmd)) {
|
|
1802
|
-
const cmdPreview = cmd.length > 60 ? cmd.substring(0, 60) + '...' : cmd;
|
|
1803
|
-
return {
|
|
1804
|
-
behavior: 'deny',
|
|
1805
|
-
message: `🚫 系统级危险操作已拦截\n命令:${cmdPreview}\n\n此类操作可能导致系统损坏,任何权限模式下都不允许执行。`
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
2027
|
}
|
|
1810
2028
|
// 默认允许
|
|
1811
2029
|
return { behavior: 'allow', updatedInput: input };
|
|
@@ -1966,7 +2184,31 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1966
2184
|
policyCode: 'managed_ec_discovery_forbidden',
|
|
1967
2185
|
};
|
|
1968
2186
|
}
|
|
1969
|
-
const
|
|
2187
|
+
const parserOptions = {
|
|
2188
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
2189
|
+
...(context.delegationCarrier
|
|
2190
|
+
? { expectedDelegationToken: context.delegationCarrier }
|
|
2191
|
+
: {}),
|
|
2192
|
+
};
|
|
2193
|
+
const parsedCodex = context.sessionId ? parseCodexToolCommand(input, parserOptions) : null;
|
|
2194
|
+
if (context.sessionId && parsedCodex && !parsedCodex.ok && parsedCodex.issue === 'untrusted-shell') {
|
|
2195
|
+
return {
|
|
2196
|
+
behavior: 'deny',
|
|
2197
|
+
input,
|
|
2198
|
+
message: '🔒 Shell 可执行文件未通过 Windows Authenticode 签名校验,禁止进入 EvolCore EC 特权路径。',
|
|
2199
|
+
policyCode: 'ec_shell_untrusted_executable',
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
if (context.sessionId && parsedCodex?.ok && parsedCodex.delegationToken
|
|
2203
|
+
&& !context.delegationCarrier) {
|
|
2204
|
+
return {
|
|
2205
|
+
behavior: 'deny',
|
|
2206
|
+
input,
|
|
2207
|
+
message: '🔒 PowerShell 命令中的 EVOLCORE_DELEGATION_TOKEN 必须与当前托管 thread carrier 绑定;无法确认时拒绝执行。',
|
|
2208
|
+
policyCode: 'delegation_token_unbound',
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
const argv = parsedCodex?.ok ? parsedCodex.argv : null;
|
|
1970
2212
|
const managedAidArgv = argv?.at(-2) === '--format' && argv.at(-1) === 'json'
|
|
1971
2213
|
? argv.slice(0, -2)
|
|
1972
2214
|
: argv;
|
|
@@ -2003,9 +2245,18 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
2003
2245
|
}
|
|
2004
2246
|
const shellDialect = stringShellCarrier?.dialect
|
|
2005
2247
|
?? (hasCodexCmdCarrierEcIntent(input) ? 'cmd' : undefined);
|
|
2006
|
-
const
|
|
2007
|
-
|
|
2008
|
-
|
|
2248
|
+
const hasPowerShellCarrierAssignment = stringShellCarrier?.dialect === 'powershell'
|
|
2249
|
+
&& hasPowerShellDelegationAssignment(stringShellCarrier.command);
|
|
2250
|
+
const ecCommand = hasPowerShellCarrierAssignment && parsedCodex
|
|
2251
|
+
? parsedCodex.ok && parsedCodex.argv[0] === 'ec'
|
|
2252
|
+
? { kind: 'literal' }
|
|
2253
|
+
: {
|
|
2254
|
+
kind: 'composite',
|
|
2255
|
+
issue: parsedCodex.ok ? 'shell-composition' : parsedCodex.issue,
|
|
2256
|
+
}
|
|
2257
|
+
: classifyEvolcoreShellCommand(policyCommand, shellDialect
|
|
2258
|
+
? { dialect: shellDialect, ...parserOptions }
|
|
2259
|
+
: parserOptions);
|
|
2009
2260
|
if (ecCommand.kind === 'literal') {
|
|
2010
2261
|
return { behavior: 'allow', input, reason: 'ec-command' };
|
|
2011
2262
|
}
|
|
@@ -2013,6 +2264,14 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
2013
2264
|
return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
|
|
2014
2265
|
}
|
|
2015
2266
|
if (ecCommand.kind === 'composite') {
|
|
2267
|
+
if (ecCommand.issue === 'untrusted-shell') {
|
|
2268
|
+
return {
|
|
2269
|
+
behavior: 'deny',
|
|
2270
|
+
input,
|
|
2271
|
+
message: '🔒 Shell 可执行文件未通过 Windows Authenticode 签名校验,禁止进入 EvolCore EC 特权路径。',
|
|
2272
|
+
policyCode: 'ec_shell_untrusted_executable',
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2016
2275
|
if (ecCommand.issue === 'unsafe-expansion') {
|
|
2017
2276
|
const message = shellDialect === 'powershell'
|
|
2018
2277
|
? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
|
|
@@ -2095,6 +2354,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
2095
2354
|
workspacePath: context.workspacePath,
|
|
2096
2355
|
root: context.root,
|
|
2097
2356
|
managedTempDir: context.managedTempDir,
|
|
2357
|
+
allowReadonlySourceDiagnostics: context.allowReadonlySourceDiagnostics,
|
|
2098
2358
|
});
|
|
2099
2359
|
if (lClass.behavior === 'deny') {
|
|
2100
2360
|
return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection', matchedPath: lClass.matchedPath };
|
|
@@ -41,28 +41,33 @@ function isTrustedRuntimeLockDir(candidate) {
|
|
|
41
41
|
if (!isWithinTmp && !isManagedSessionSibling)
|
|
42
42
|
return false;
|
|
43
43
|
try {
|
|
44
|
+
// Windows Stats.mode does not describe ACL privacy. The managed runtime
|
|
45
|
+
// roots are protected by Windows ACL inheritance, so apply POSIX
|
|
46
|
+
// ownership and permission-bit checks only where those fields are
|
|
47
|
+
// meaningful.
|
|
48
|
+
const isWindows = process.platform === 'win32';
|
|
44
49
|
const tmpStat = fs.lstatSync(resolvedTmpRoot);
|
|
45
50
|
if (!tmpStat.isDirectory() || tmpStat.isSymbolicLink())
|
|
46
51
|
return false;
|
|
47
|
-
if (typeof process.getuid === 'function' && tmpStat.uid !== process.getuid())
|
|
52
|
+
if (!isWindows && typeof process.getuid === 'function' && tmpStat.uid !== process.getuid())
|
|
48
53
|
return false;
|
|
49
|
-
if ((tmpStat.mode & 0o077) !== 0)
|
|
54
|
+
if (!isWindows && (tmpStat.mode & 0o077) !== 0)
|
|
50
55
|
return false;
|
|
51
56
|
if (isManagedSessionSibling) {
|
|
52
57
|
const parentStat = fs.lstatSync(tmpParent);
|
|
53
58
|
if (!parentStat.isDirectory() || parentStat.isSymbolicLink())
|
|
54
59
|
return false;
|
|
55
|
-
if (typeof process.getuid === 'function' && parentStat.uid !== process.getuid())
|
|
60
|
+
if (!isWindows && typeof process.getuid === 'function' && parentStat.uid !== process.getuid())
|
|
56
61
|
return false;
|
|
57
|
-
if ((parentStat.mode & 0o077) !== 0)
|
|
62
|
+
if (!isWindows && (parentStat.mode & 0o077) !== 0)
|
|
58
63
|
return false;
|
|
59
64
|
}
|
|
60
65
|
const stat = fs.lstatSync(resolved);
|
|
61
66
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
62
67
|
return false;
|
|
63
|
-
if (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
68
|
+
if (!isWindows && typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
64
69
|
return false;
|
|
65
|
-
return (stat.mode & 0o077) === 0;
|
|
70
|
+
return isWindows || (stat.mode & 0o077) === 0;
|
|
66
71
|
}
|
|
67
72
|
catch {
|
|
68
73
|
return false;
|
|
@@ -93,8 +98,8 @@ export function ensureManagedLockParent(lockPath) {
|
|
|
93
98
|
}
|
|
94
99
|
const stat = fs.lstatSync(current);
|
|
95
100
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
96
|
-
|| (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
97
|
-
|| (stat.mode & 0o077) !== 0) {
|
|
101
|
+
|| (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
102
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
|
|
98
103
|
throw new Error(`managed runtime lock path is not a private real directory: ${current}`);
|
|
99
104
|
}
|
|
100
105
|
}
|