evolcore 0.0.13 → 0.0.14
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 +15 -0
- package/bin/codex-managed-hook.mjs +4 -1
- package/bin/install-codex-managed-hooks.mjs +201 -0
- package/dist/agents/claude-runner.js +53 -5
- package/dist/agents/codex-app-server-client.js +123 -2
- package/dist/agents/codex-runner.js +149 -30
- package/dist/agents/ecagent-runner.js +17 -1
- package/dist/agents/gemini-runner.js +9 -4
- package/dist/aun/msg/managed-operation.js +63 -3
- package/dist/channels/aun.js +144 -15
- package/dist/channels/daemon.js +2 -0
- package/dist/cli/aun-commands.js +1 -1
- package/dist/cli/fs-command.js +46 -9
- package/dist/cli/task-context.js +172 -0
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/config-manager.js +6 -2
- package/dist/config/contact-book-store.js +7 -2
- package/dist/core/auth/auth-gateway.js +1 -0
- package/dist/core/auth/authorization-audit.js +32 -0
- package/dist/core/auth/operation-catalog.js +3 -3
- package/dist/core/bootstrap-service.js +7 -1
- package/dist/core/command/command-handler.js +3 -0
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/message/im-renderer.js +15 -1
- package/dist/core/message/message-bridge.js +5 -2
- package/dist/core/message/response-engine.js +138 -10
- package/dist/core/permission/ec-command-parser.js +556 -4
- package/dist/core/permission/tool-policy.js +17 -29
- package/dist/core/runtime-lock.js +101 -0
- package/dist/index.js +30 -3
- package/dist/response-system/engines/v1/proactive-flow.js +92 -8
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/history.js +42 -7
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/logger.js +37 -4
- package/kits/templates/roles/admin.json +2 -0
- package/kits/templates/roles/member.json +1 -0
- package/package.json +1 -1
|
@@ -13,15 +13,17 @@ import { CodexAppServerClient } from './codex-app-server-client.js';
|
|
|
13
13
|
import { resolveOpenaiConfig } from './baseagent.js';
|
|
14
14
|
import { logger } from '../utils/logger.js';
|
|
15
15
|
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
16
|
+
import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
16
17
|
import { isRetryableError } from '../utils/error-utils.js';
|
|
17
18
|
import { renderActionAsText } from '../core/interaction-router.js';
|
|
18
19
|
import { buildEnvelope, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
19
20
|
import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
|
|
20
21
|
import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
|
|
21
22
|
import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
|
|
22
|
-
import { classifyEvolcoreShellCommand, parseLiteralShellArgv, unwrapCodexShellCommandArgv } from '../core/permission/ec-command-parser.js';
|
|
23
|
+
import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, parseLiteralShellArgv, unwrapCodexShellCommandArgv, } from '../core/permission/ec-command-parser.js';
|
|
23
24
|
import { compareVersions } from '../utils/npm-ops.js';
|
|
24
25
|
import { resolvePaths, resolveRoot } from '../paths.js';
|
|
26
|
+
import { ensureProcessManagedTempDir } from '../cli/task-context.js';
|
|
25
27
|
import { buildSessionTurnList } from '../core/session/session-turns.js';
|
|
26
28
|
import { execFileSync } from 'child_process';
|
|
27
29
|
import { execCodexCliSync, resolveCodexCliPath } from '../utils/codex-cli.js';
|
|
@@ -151,7 +153,7 @@ const CODEX_DELEGATION_CARRIER_METADATA_KEY = '__codexDelegationCarrierV1';
|
|
|
151
153
|
let codexCatalogCache = null;
|
|
152
154
|
// The permission bridge is version-locked to the audited approval schema and
|
|
153
155
|
// managed PreToolUse hook behavior used by proactive first-tool enforcement.
|
|
154
|
-
export const MIN_CODEX_CLI_VERSION = '0.
|
|
156
|
+
export const MIN_CODEX_CLI_VERSION = '0.144.1';
|
|
155
157
|
export function parseCodexCliVersion(output) {
|
|
156
158
|
const match = output.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
|
|
157
159
|
return match?.[1] ?? null;
|
|
@@ -326,7 +328,7 @@ export class CodexRunner {
|
|
|
326
328
|
allowManagedTmpDir: true,
|
|
327
329
|
...(managedTempDir ? { managedTempDir } : {}),
|
|
328
330
|
});
|
|
329
|
-
if (argv
|
|
331
|
+
if (argv && containsLiteralManagedTmpDirOutsideSendContent(argv))
|
|
330
332
|
return undefined;
|
|
331
333
|
return argv?.[0] === 'ec' ? argv : undefined;
|
|
332
334
|
}
|
|
@@ -335,8 +337,12 @@ export class CodexRunner {
|
|
|
335
337
|
&& toolInput.commandArgv.every(value => typeof value === 'string')
|
|
336
338
|
? toolInput.commandArgv
|
|
337
339
|
: undefined;
|
|
338
|
-
if (explicitArgv?.[0] === 'ec')
|
|
339
|
-
return explicitArgv
|
|
340
|
+
if (explicitArgv?.[0] === 'ec') {
|
|
341
|
+
return explicitArgv.some(value => value.includes('\0'))
|
|
342
|
+
|| containsLiteralManagedTmpDirOutsideSendContent(explicitArgv)
|
|
343
|
+
? undefined
|
|
344
|
+
: explicitArgv;
|
|
345
|
+
}
|
|
340
346
|
const explicitWrappedCommand = explicitArgv
|
|
341
347
|
? unwrapCodexShellCommandArgv(explicitArgv)
|
|
342
348
|
: undefined;
|
|
@@ -358,17 +364,34 @@ export class CodexRunner {
|
|
|
358
364
|
? undefined
|
|
359
365
|
: this.approvedLiteralEvolcoreCommandArgv(wrappedCommand, managedTempDir);
|
|
360
366
|
}
|
|
361
|
-
|
|
367
|
+
isManagedEvolcoreCommandIntent(toolInput) {
|
|
368
|
+
if (Array.isArray(toolInput.commandArgv)) {
|
|
369
|
+
const rawArgv = toolInput.commandArgv;
|
|
370
|
+
if (rawArgv[0] === 'ec')
|
|
371
|
+
return true;
|
|
372
|
+
if (!rawArgv.every(value => typeof value === 'string'))
|
|
373
|
+
return false;
|
|
374
|
+
const wrappedCommand = unwrapCodexShellCommandArgv(rawArgv);
|
|
375
|
+
return wrappedCommand !== undefined
|
|
376
|
+
&& classifyEvolcoreShellCommand(wrappedCommand).kind !== 'none';
|
|
377
|
+
}
|
|
378
|
+
const command = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
379
|
+
return classifyEvolcoreShellCommand(command).kind !== 'none';
|
|
380
|
+
}
|
|
381
|
+
armApprovedDelegationCommand(sessionId, argv) {
|
|
362
382
|
const threadId = this.activeSessions.get(sessionId);
|
|
363
383
|
const carrierToken = threadId ? this.threadDelegationCarriers.get(threadId) : undefined;
|
|
364
|
-
const argv = this.approvedEvolcoreCommandArgv(toolInput, this.getManagedTempDir(sessionId));
|
|
365
|
-
if (!carrierToken || !argv)
|
|
366
|
-
return false;
|
|
367
384
|
const commandHash = hashDelegatedCommandArgv(argv);
|
|
368
385
|
const arm = this.permissionContexts.get(sessionId)?.armApprovedDelegationCommand;
|
|
369
|
-
if (!commandHash || !arm)
|
|
386
|
+
if (!carrierToken || !commandHash || !arm)
|
|
387
|
+
return false;
|
|
388
|
+
try {
|
|
389
|
+
return arm(carrierToken, commandHash);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
logger.warn(`[CodexRunner] delegation arm callback failed: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
370
393
|
return false;
|
|
371
|
-
|
|
394
|
+
}
|
|
372
395
|
}
|
|
373
396
|
async readThreadDelegationCarrier(appServer, threadId) {
|
|
374
397
|
try {
|
|
@@ -792,6 +815,8 @@ export class CodexRunner {
|
|
|
792
815
|
...(mode !== 'readonly' && managedTempDir ? {
|
|
793
816
|
[managedTempDir]: 'write',
|
|
794
817
|
[path.join(managedTempDir, '**')]: 'write',
|
|
818
|
+
[path.join(path.dirname(managedTempDir), 'evolcore-locks')]: 'write',
|
|
819
|
+
[path.join(path.dirname(managedTempDir), 'evolcore-locks', '**')]: 'write',
|
|
795
820
|
} : {}),
|
|
796
821
|
},
|
|
797
822
|
network,
|
|
@@ -831,6 +856,17 @@ export class CodexRunner {
|
|
|
831
856
|
}
|
|
832
857
|
setSendPrompt(fn) { this.sendPromptFn = fn; }
|
|
833
858
|
setPermissionContext(sessionId, context) { this.permissionContexts.set(sessionId, context); }
|
|
859
|
+
async assertProactiveHookAvailability(sessionId, appServer) {
|
|
860
|
+
if (this.permissionContexts.get(sessionId)?.chatmode !== 'proactive')
|
|
861
|
+
return;
|
|
862
|
+
if (process.platform === 'win32') {
|
|
863
|
+
await appServer.assertManagedHooksAvailable();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (process.platform !== 'linux') {
|
|
867
|
+
throw new Error('Codex proactive managed PreToolUse enforcement requires Linux or an installed Windows managed hook');
|
|
868
|
+
}
|
|
869
|
+
}
|
|
834
870
|
async evaluatePreToolUse(threadId, toolName, toolInput) {
|
|
835
871
|
const sessionKey = this.findSessionKeyByThread(threadId);
|
|
836
872
|
const activeThread = this.activeSessions.get(sessionKey);
|
|
@@ -995,9 +1031,6 @@ export class CodexRunner {
|
|
|
995
1031
|
}
|
|
996
1032
|
async runQueryLocked(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
|
|
997
1033
|
let agentSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
|
|
998
|
-
if (this.permissionContexts.get(sessionId)?.chatmode === 'proactive' && process.platform !== 'linux') {
|
|
999
|
-
throw new Error('Codex proactive managed PreToolUse enforcement currently requires Linux');
|
|
1000
|
-
}
|
|
1001
1034
|
const resumingThread = !!agentSessionId;
|
|
1002
1035
|
const callModel = modelOverride?.model || this.model;
|
|
1003
1036
|
const callEffort = modelOverride?.effortMode === 'model_default'
|
|
@@ -1017,6 +1050,7 @@ export class CodexRunner {
|
|
|
1017
1050
|
const effectiveApprovalPolicy = callApprovalPolicy;
|
|
1018
1051
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
1019
1052
|
const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, callMode);
|
|
1053
|
+
await this.assertProactiveHookAvailability(sessionId, appServer);
|
|
1020
1054
|
const managedTempInstruction = this.managedTempInstruction(sessionId);
|
|
1021
1055
|
const developerInstructions = [systemPromptAppend, managedTempInstruction].filter(Boolean).join('\n\n') || undefined;
|
|
1022
1056
|
let knownDelegationCarrier = agentSessionId
|
|
@@ -1086,7 +1120,7 @@ export class CodexRunner {
|
|
|
1086
1120
|
const controller = new AbortController();
|
|
1087
1121
|
this.activeAbortControllers.set(sessionId, controller);
|
|
1088
1122
|
const tempFiles = [];
|
|
1089
|
-
const input = this.buildAppServerInput(prompt, images, tempFiles);
|
|
1123
|
+
const input = this.buildAppServerInput(prompt, images, tempFiles, sessionId, runtimeEnv);
|
|
1090
1124
|
const queue = new AsyncEventQueue();
|
|
1091
1125
|
controller.signal.addEventListener('abort', () => queue.end(), { once: true });
|
|
1092
1126
|
const state = {
|
|
@@ -1301,6 +1335,7 @@ export class CodexRunner {
|
|
|
1301
1335
|
const executionSandbox = this.buildExecutionSandboxOptions(_sessionId, compactMode, _projectPath);
|
|
1302
1336
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(_projectPath);
|
|
1303
1337
|
const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, _projectPath, capabilityConfig, compactMode);
|
|
1338
|
+
await this.assertProactiveHookAvailability(_sessionId, appServer);
|
|
1304
1339
|
const compactDelegationCarrier = this.threadDelegationCarriers.get(agentSessionId)
|
|
1305
1340
|
?? this.createDelegationCarrier();
|
|
1306
1341
|
await appServer.threadResume(agentSessionId, _projectPath, {
|
|
@@ -1464,6 +1499,8 @@ export class CodexRunner {
|
|
|
1464
1499
|
const capabilityConfig = await this.resolveCapabilityThreadConfig(projectPath);
|
|
1465
1500
|
const appServer = this.getAppServerClient();
|
|
1466
1501
|
const externalToolConfig = await this.resolveExternalToolApprovalConfig(appServer, projectPath, capabilityConfig, mode);
|
|
1502
|
+
if (sessionKey)
|
|
1503
|
+
await this.assertProactiveHookAvailability(sessionKey, appServer);
|
|
1467
1504
|
const delegationCarrier = this.createDelegationCarrier();
|
|
1468
1505
|
const response = await appServer.threadFork(agentSessionId, projectPath, title, {
|
|
1469
1506
|
model: this.model,
|
|
@@ -1656,31 +1693,77 @@ export class CodexRunner {
|
|
|
1656
1693
|
catch (error) {
|
|
1657
1694
|
logger.warn(`[CodexRunner] failed to record policy-hook denial: session=${sessionKey} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1658
1695
|
}
|
|
1659
|
-
|
|
1696
|
+
const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
1697
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'policy', policyResult.policyCode ?? 'session_policy_hook');
|
|
1698
|
+
return response;
|
|
1660
1699
|
}
|
|
1661
1700
|
const summary = this.summarizeAppServerRequest(request.method, params);
|
|
1662
1701
|
const reason = params.reason || params.decisionReason || undefined;
|
|
1663
1702
|
const workspacePath = this.resolvePermissionWorkspacePath(params);
|
|
1664
1703
|
if (!workspacePath) {
|
|
1665
1704
|
logger.warn(`[CodexRunner] approval denied because thread workspace is unknown: method=${request.method} thread=${params.threadId ?? params.conversationId ?? '<missing>'}`);
|
|
1666
|
-
|
|
1705
|
+
const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
1706
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', 'approval_workspace_unknown');
|
|
1707
|
+
return response;
|
|
1667
1708
|
}
|
|
1668
1709
|
const operationCwd = this.resolvePermissionOperationCwd(params, workspacePath, toolName);
|
|
1669
1710
|
const sessionMode = this.chatModes.get(sessionKey) ?? this.currentMode;
|
|
1670
1711
|
logger.info(`[CodexRunner] app-server approval request id=${request.id} method=${request.method} session=${sessionKey} mode=${sessionMode} tool=${toolName} summary=${summary}`);
|
|
1712
|
+
const isCommandApproval = request.method === 'item/commandExecution/requestApproval'
|
|
1713
|
+
|| request.method === 'execCommandApproval';
|
|
1714
|
+
const managedEcIntent = isCommandApproval && this.isManagedEvolcoreCommandIntent(toolInput);
|
|
1715
|
+
const approvedCommand = isCommandApproval
|
|
1716
|
+
? this.approvedEvolcoreCommandArgv(toolInput, this.getManagedTempDir(sessionKey))
|
|
1717
|
+
: undefined;
|
|
1718
|
+
const denyInfrastructure = async (policyCode, message) => {
|
|
1719
|
+
logger.warn(`[CodexRunner] ${message}: session=${sessionKey} requestId=${request.id ?? '<missing>'}`);
|
|
1720
|
+
try {
|
|
1721
|
+
await permissionContext?.recordExecutionAnomaly?.({
|
|
1722
|
+
code: 'operation_blocked',
|
|
1723
|
+
severity: 'warning',
|
|
1724
|
+
phase: 'execution',
|
|
1725
|
+
occurredAt: Date.now(),
|
|
1726
|
+
toolName,
|
|
1727
|
+
...(request.id !== undefined ? { requestId: String(request.id) } : {}),
|
|
1728
|
+
policyCode,
|
|
1729
|
+
decisionSource: 'infrastructure',
|
|
1730
|
+
agentAid: permissionContext?.selfAid,
|
|
1731
|
+
sessionId: sessionKey,
|
|
1732
|
+
permissionMode: normalizePermissionMode(sessionMode).mode,
|
|
1733
|
+
summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
|
|
1734
|
+
effect: 'operation_skipped',
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
catch (error) {
|
|
1738
|
+
logger.warn(`[CodexRunner] failed to record infrastructure approval denial: session=${sessionKey} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1739
|
+
}
|
|
1740
|
+
const response = this.toAppServerApprovalResponse(request.method, 'deny', toolInput, unattended);
|
|
1741
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, 'deny', 'infrastructure', policyCode);
|
|
1742
|
+
return response;
|
|
1743
|
+
};
|
|
1744
|
+
if (managedEcIntent && !approvedCommand) {
|
|
1745
|
+
return await denyInfrastructure('ec_command_not_canonical', 'EC command approval denied because no canonical argv could be derived');
|
|
1746
|
+
}
|
|
1671
1747
|
try {
|
|
1672
1748
|
const decision = await this.resolvePermissionDecision(sessionKey, toolName, toolInput, summary, reason, workspacePath, operationCwd, request.id);
|
|
1673
|
-
if (decision !== 'deny'
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1749
|
+
if (decision !== 'deny' && managedEcIntent && approvedCommand) {
|
|
1750
|
+
const carrierThreadIds = [
|
|
1751
|
+
this.activeSessions.get(sessionKey),
|
|
1752
|
+
typeof params.threadId === 'string' ? params.threadId : undefined,
|
|
1753
|
+
typeof params.conversationId === 'string' ? params.conversationId : undefined,
|
|
1754
|
+
];
|
|
1755
|
+
const hasDelegationCarrier = carrierThreadIds.some(threadId => !!threadId && this.threadDelegationCarriers.has(threadId));
|
|
1756
|
+
const canArmDelegation = typeof permissionContext?.armApprovedDelegationCommand === 'function';
|
|
1757
|
+
// A carrier-bearing thread is managed even if its permission context
|
|
1758
|
+
// was not wired with an arm callback; it must fail closed here rather
|
|
1759
|
+
// than reaching daemon IPC with an unarmed carrier.
|
|
1760
|
+
if ((hasDelegationCarrier || canArmDelegation)
|
|
1761
|
+
&& !this.armApprovedDelegationCommand(sessionKey, approvedCommand)) {
|
|
1762
|
+
return await denyInfrastructure('delegation_not_armed', 'EC command approval failed to arm delegation');
|
|
1681
1763
|
}
|
|
1682
1764
|
}
|
|
1683
1765
|
const response = this.toAppServerApprovalResponse(request.method, decision, toolInput, unattended);
|
|
1766
|
+
this.logAppServerApprovalAudit(request, sessionKey, toolName, decision === 'deny' ? 'deny' : 'allow', decision === 'deny' ? (sessionMode === 'auto' || sessionMode === 'readonly' ? 'policy' : 'approval') : 'approval');
|
|
1684
1767
|
logger.info(`[CodexRunner] app-server approval response id=${request.id} method=${request.method} decision=${decision} response=${JSON.stringify(response)}`);
|
|
1685
1768
|
return response;
|
|
1686
1769
|
}
|
|
@@ -1690,6 +1773,18 @@ export class CodexRunner {
|
|
|
1690
1773
|
throw error;
|
|
1691
1774
|
}
|
|
1692
1775
|
}
|
|
1776
|
+
logAppServerApprovalAudit(request, sessionKey, toolName, decision, decisionSource, policyCode) {
|
|
1777
|
+
logger.info(`[CodexRunner] app-server approval audit ${JSON.stringify({
|
|
1778
|
+
requestId: request.id !== undefined ? String(request.id) : undefined,
|
|
1779
|
+
method: request.method,
|
|
1780
|
+
sessionId: sessionKey,
|
|
1781
|
+
toolName,
|
|
1782
|
+
decision,
|
|
1783
|
+
decisionSource,
|
|
1784
|
+
...(policyCode ? { policyCode } : {}),
|
|
1785
|
+
executed: false,
|
|
1786
|
+
})}`);
|
|
1787
|
+
}
|
|
1693
1788
|
classifyMcpApprovalLabel(label) {
|
|
1694
1789
|
const normalized = label.trim().toLowerCase().replace(/[\s_-]+/g, ' ');
|
|
1695
1790
|
const negative = /\b(decline|deny|reject|cancel|no|block|stop)\b|do not|don't|not\s+(?:accept|approve|allow)|拒绝|不同意|不允许|取消|否|停止/i.test(normalized);
|
|
@@ -2221,6 +2316,22 @@ export class CodexRunner {
|
|
|
2221
2316
|
const rawMode = this.chatModes.get(sessionKey) ?? this.currentMode;
|
|
2222
2317
|
const mode = normalizePermissionMode(rawMode).mode;
|
|
2223
2318
|
const recordBlockedOperation = async (policyCode, operationInput = toolInput) => {
|
|
2319
|
+
const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
|
|
2320
|
+
auditToolPreflightDenial({
|
|
2321
|
+
toolName,
|
|
2322
|
+
policyCode,
|
|
2323
|
+
reason: 'policy denied',
|
|
2324
|
+
summary,
|
|
2325
|
+
sessionId: sessionKey,
|
|
2326
|
+
agentAid: permissionContext?.selfAid,
|
|
2327
|
+
permissionMode: mode,
|
|
2328
|
+
channel: permissionContext?.channel,
|
|
2329
|
+
actorId: permissionContext?.userId,
|
|
2330
|
+
role: permissionContext?.role,
|
|
2331
|
+
selfAid: permissionContext?.selfAid,
|
|
2332
|
+
requestId: typeof requestId === 'string' || typeof requestId === 'number' ? String(requestId) : undefined,
|
|
2333
|
+
taskId: permissionContext?.taskId,
|
|
2334
|
+
});
|
|
2224
2335
|
try {
|
|
2225
2336
|
await permissionContext?.recordExecutionAnomaly?.({
|
|
2226
2337
|
code: 'operation_blocked',
|
|
@@ -2233,7 +2344,7 @@ export class CodexRunner {
|
|
|
2233
2344
|
decisionSource: 'policy',
|
|
2234
2345
|
agentAid: permissionContext?.selfAid,
|
|
2235
2346
|
permissionMode: mode,
|
|
2236
|
-
summary
|
|
2347
|
+
summary,
|
|
2237
2348
|
effect: 'operation_skipped',
|
|
2238
2349
|
});
|
|
2239
2350
|
}
|
|
@@ -2525,11 +2636,15 @@ export class CodexRunner {
|
|
|
2525
2636
|
this.onCompactStart = callback;
|
|
2526
2637
|
}
|
|
2527
2638
|
// ── Event stream transformation ──
|
|
2528
|
-
buildAppServerInput(prompt, images, tempFiles) {
|
|
2639
|
+
buildAppServerInput(prompt, images, tempFiles, sessionId, runtimeEnv) {
|
|
2529
2640
|
const input = [{ type: 'text', text: prompt, text_elements: [] }];
|
|
2530
2641
|
if (!images?.length)
|
|
2531
2642
|
return input;
|
|
2532
|
-
const tmpDir =
|
|
2643
|
+
const tmpDir = runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR
|
|
2644
|
+
?? (sessionId ? this.getManagedTempDir(sessionId) : undefined)
|
|
2645
|
+
?? process.env.TMPDIR;
|
|
2646
|
+
if (!tmpDir || !path.isAbsolute(tmpDir))
|
|
2647
|
+
throw new Error('managed TMPDIR is unset or not absolute');
|
|
2533
2648
|
for (let i = 0; i < images.length; i++) {
|
|
2534
2649
|
const img = images[i];
|
|
2535
2650
|
const ext = MIME_EXT[img.mimeType || ''] || '.jpg';
|
|
@@ -3018,7 +3133,8 @@ export class CodexRunner {
|
|
|
3018
3133
|
}
|
|
3019
3134
|
managedTempParent() {
|
|
3020
3135
|
const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
|
|
3021
|
-
|
|
3136
|
+
const base = ensureProcessManagedTempDir();
|
|
3137
|
+
return path.join(resolveProtectedCandidate(base), `evolcore-codex-${uid}`);
|
|
3022
3138
|
}
|
|
3023
3139
|
ensurePrivateDirectory(directory) {
|
|
3024
3140
|
try {
|
|
@@ -3055,6 +3171,7 @@ export class CodexRunner {
|
|
|
3055
3171
|
try {
|
|
3056
3172
|
const parent = this.managedTempParent();
|
|
3057
3173
|
this.ensurePrivateDirectory(parent);
|
|
3174
|
+
this.ensurePrivateDirectory(path.join(parent, 'evolcore-locks'));
|
|
3058
3175
|
// Include a per-runner nonce so a daemon restart never reuses contents
|
|
3059
3176
|
// left behind by an unclean shutdown for the same EvolCore session id.
|
|
3060
3177
|
const digest = createHash('sha256')
|
|
@@ -3078,7 +3195,7 @@ export class CodexRunner {
|
|
|
3078
3195
|
return isTrustedManagedTempDirectory(directory, this.managedTempParent());
|
|
3079
3196
|
}
|
|
3080
3197
|
getManagedTempDir(sessionId) {
|
|
3081
|
-
const directory = this.
|
|
3198
|
+
const directory = this.ensureSessionTempDir(sessionId);
|
|
3082
3199
|
if (!directory || !this.isTrustedSessionTempDir(directory))
|
|
3083
3200
|
return undefined;
|
|
3084
3201
|
return resolveProtectedCandidate(directory);
|
|
@@ -3117,6 +3234,8 @@ export class CodexRunner {
|
|
|
3117
3234
|
set: {
|
|
3118
3235
|
EVOLCORE_SESSION_ID: sessionId,
|
|
3119
3236
|
EVOLCORE_HOME: resolveRoot(),
|
|
3237
|
+
...(managedTempDir ? { EVOLCORE_SESSION_RUNTIME_DIR: managedTempDir } : {}),
|
|
3238
|
+
...(managedTempDir ? { EVOLCORE_RUNTIME_LOCK_DIR: path.join(path.dirname(managedTempDir), 'evolcore-locks') } : {}),
|
|
3120
3239
|
...(delegationCarrier ? { [AGENT_DELEGATION_TOKEN_ENV]: delegationCarrier } : {}),
|
|
3121
3240
|
...(managedTempDir ? { TMPDIR: managedTempDir } : {}),
|
|
3122
3241
|
},
|
|
@@ -10,6 +10,7 @@ import { normalizePermissionMode } from '../core/permission/mode.js';
|
|
|
10
10
|
import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
11
11
|
import { logger } from '../utils/logger.js';
|
|
12
12
|
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
13
|
+
import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
13
14
|
import { resolveEcagentConfig } from './baseagent.js';
|
|
14
15
|
import { buildModelRequestHeaders } from './request-identity.js';
|
|
15
16
|
const PROVIDER_ID = 'evolcore-gateway';
|
|
@@ -959,6 +960,21 @@ export class EcagentRunner {
|
|
|
959
960
|
async authorizeTool(sessionId, projectPath, mode, toolName, input) {
|
|
960
961
|
const permissionContext = this.permissionContexts.get(sessionId);
|
|
961
962
|
const recordBlockedOperation = async (policyCode, operationInput = input) => {
|
|
963
|
+
const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
|
|
964
|
+
auditToolPreflightDenial({
|
|
965
|
+
toolName,
|
|
966
|
+
policyCode,
|
|
967
|
+
reason: 'policy denied',
|
|
968
|
+
summary,
|
|
969
|
+
sessionId,
|
|
970
|
+
agentAid: permissionContext?.selfAid,
|
|
971
|
+
permissionMode: mode,
|
|
972
|
+
channel: permissionContext?.channel,
|
|
973
|
+
actorId: permissionContext?.userId,
|
|
974
|
+
role: permissionContext?.role,
|
|
975
|
+
selfAid: permissionContext?.selfAid,
|
|
976
|
+
taskId: permissionContext?.taskId,
|
|
977
|
+
});
|
|
962
978
|
try {
|
|
963
979
|
await permissionContext?.recordExecutionAnomaly?.({
|
|
964
980
|
code: 'operation_blocked',
|
|
@@ -970,7 +986,7 @@ export class EcagentRunner {
|
|
|
970
986
|
decisionSource: 'policy',
|
|
971
987
|
agentAid: permissionContext?.selfAid,
|
|
972
988
|
permissionMode: mode,
|
|
973
|
-
summary
|
|
989
|
+
summary,
|
|
974
990
|
effect: 'operation_skipped',
|
|
975
991
|
});
|
|
976
992
|
}
|
|
@@ -279,11 +279,16 @@ export class GeminiRunner {
|
|
|
279
279
|
const requestedPermissionMode = modelOverride?.permissionMode || this.currentMode;
|
|
280
280
|
const permissionProfile = resolveGeminiPermissionProfile(requestedPermissionMode);
|
|
281
281
|
const callModel = modelOverride?.model || this.model;
|
|
282
|
+
const runtimeDir = runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR;
|
|
283
|
+
const managedTmpDir = runtimeDir ?? process.env.TMPDIR;
|
|
284
|
+
if (!managedTmpDir || !path.isAbsolute(managedTmpDir))
|
|
285
|
+
throw new Error('managed TMPDIR is unset or not absolute');
|
|
286
|
+
const runtimeLockDir = runtimeEnv?.EVOLCORE_RUNTIME_LOCK_DIR;
|
|
282
287
|
const geminiHome = path.join(os.homedir(), '.gemini');
|
|
283
288
|
fs.mkdirSync(geminiHome, { recursive: true, mode: 0o700 });
|
|
284
289
|
const sandboxProbe = buildBubblewrapCommand(this.resolved.cliPath, [], {
|
|
285
290
|
projectPath,
|
|
286
|
-
writablePaths: [geminiHome],
|
|
291
|
+
writablePaths: [geminiHome, ...(runtimeDir ? [runtimeDir] : []), ...(runtimeLockDir ? [runtimeLockDir] : [])],
|
|
287
292
|
});
|
|
288
293
|
if (!sandboxProbe && workspaceContainsHClassPaths(projectPath)) {
|
|
289
294
|
throw new Error('Gemini 缺少可用的路径级隔离运行时,且项目覆盖 EvolCore 受保护根;已拒绝启动本轮任务');
|
|
@@ -303,13 +308,13 @@ export class GeminiRunner {
|
|
|
303
308
|
if (hasStandardGeminiAdminPolicy()) {
|
|
304
309
|
throw new Error('Gemini 系统级 admin policy 会忽略 EvolCore 的临时安全策略,已拒绝启动本轮任务');
|
|
305
310
|
}
|
|
306
|
-
const policyPath = path.join(
|
|
311
|
+
const policyPath = path.join(managedTmpDir, `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
|
|
307
312
|
fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile), { mode: 0o600, flag: 'wx' });
|
|
308
313
|
tempFiles.push(policyPath);
|
|
309
314
|
args.push(...buildGeminiPermissionArgs(permissionProfile, policyPath, !!sandboxProbe));
|
|
310
315
|
}
|
|
311
316
|
if (images?.length) {
|
|
312
|
-
const tmpDir =
|
|
317
|
+
const tmpDir = managedTmpDir;
|
|
313
318
|
const fileParts = [];
|
|
314
319
|
for (let i = 0; i < images.length; i++) {
|
|
315
320
|
const img = images[i];
|
|
@@ -337,7 +342,7 @@ export class GeminiRunner {
|
|
|
337
342
|
const env = this.buildAgentEnv(sessionId, runtimeEnv);
|
|
338
343
|
const sandboxedCommand = buildBubblewrapCommand(this.resolved.cliPath, args, {
|
|
339
344
|
projectPath,
|
|
340
|
-
writablePaths: [geminiHome],
|
|
345
|
+
writablePaths: [geminiHome, ...(runtimeDir ? [runtimeDir] : []), ...(runtimeLockDir ? [runtimeLockDir] : [])],
|
|
341
346
|
readonlyPaths: tempFiles,
|
|
342
347
|
});
|
|
343
348
|
const child = spawn(sandboxedCommand?.command ?? this.resolved.cliPath, sandboxedCommand?.args ?? args, {
|
|
@@ -561,7 +561,7 @@ export class ManagedFsOperationError extends Error {
|
|
|
561
561
|
}
|
|
562
562
|
const FS_VALUE_FLAGS = new Set([
|
|
563
563
|
'--as', '--format', '--max-bytes', '--head-bytes', '--name', '--pattern', '--type', '--node-type',
|
|
564
|
-
'--size', '--mtime', '--page', '--page-size',
|
|
564
|
+
'--size', '--mtime', '--page', '--page-size', '-m', '-x',
|
|
565
565
|
]);
|
|
566
566
|
const FS_BOOLEAN_FLAGS = new Set(['-p', '--parents', '-r', '--recursive', '-f', '--force', '--overwrite']);
|
|
567
567
|
function failFs(code, reason) {
|
|
@@ -599,7 +599,7 @@ function parseRemote(value, allowEmpty) {
|
|
|
599
599
|
export function resolveManagedFsOperation(argv) {
|
|
600
600
|
const raw = argv[0] === 'fs' ? argv.slice(1) : argv;
|
|
601
601
|
const rawKind = raw[0];
|
|
602
|
-
if (!rawKind || !['ls', 'stat', 'lstat', 'cat', 'find', 'df', 'mkdir', 'cp', 'mv', 'rm'].includes(rawKind)) {
|
|
602
|
+
if (!rawKind || !['ls', 'stat', 'lstat', 'cat', 'find', 'df', 'getfacl', 'setfacl', 'mkdir', 'cp', 'mv', 'rm'].includes(rawKind)) {
|
|
603
603
|
return failFs('NOT_ALLOWED', `unsupported managed fs command: ${rawKind || '<missing>'}`);
|
|
604
604
|
}
|
|
605
605
|
const values = new Map();
|
|
@@ -652,9 +652,10 @@ export function resolveManagedFsOperation(argv) {
|
|
|
652
652
|
find: new Set(['--as', '--format', '--name', '--pattern', '--type', '--node-type', '--size', '--mtime', '--page', '--page-size']),
|
|
653
653
|
df: new Set(['--as', '--format']), mkdir: new Set(['--as', '--format']), cp: new Set(['--as', '--format']),
|
|
654
654
|
mv: new Set(['--as', '--format']), rm: new Set(['--as', '--format']),
|
|
655
|
+
getfacl: new Set(['--as', '--format']), setfacl: new Set(['--as', '--format', '-m', '-x']),
|
|
655
656
|
};
|
|
656
657
|
const allowedBoolean = {
|
|
657
|
-
ls: new Set(), stat: new Set(), lstat: new Set(), cat: new Set(), find: new Set(), df: new Set(),
|
|
658
|
+
ls: new Set(), stat: new Set(), lstat: new Set(), cat: new Set(), find: new Set(), df: new Set(), getfacl: new Set(), setfacl: new Set(),
|
|
658
659
|
mkdir: new Set(['-p', '--parents']), cp: new Set(['-r', '--recursive', '-f', '--force', '--overwrite']),
|
|
659
660
|
mv: new Set(['-f', '--force', '--overwrite']), rm: new Set(['-r', '--recursive', '-f', '--force', '--overwrite']),
|
|
660
661
|
};
|
|
@@ -667,6 +668,35 @@ export function resolveManagedFsOperation(argv) {
|
|
|
667
668
|
const overwrite = booleanFlags.has('-f') || booleanFlags.has('--force') || booleanFlags.has('--overwrite');
|
|
668
669
|
const recursive = booleanFlags.has('-r') || booleanFlags.has('--recursive');
|
|
669
670
|
const remoteOrError = (value, allowEmpty = false) => parseRemote(value, allowEmpty);
|
|
671
|
+
if (rawKind === 'getfacl') {
|
|
672
|
+
if (values.has('-m') || values.has('-x'))
|
|
673
|
+
return failFs('NOT_ALLOWED', 'getfacl does not accept ACL mutation flags');
|
|
674
|
+
const remote = remoteOrError(positionals[0]);
|
|
675
|
+
if (!('groupId' in remote))
|
|
676
|
+
return remote;
|
|
677
|
+
return { ok: true, command: {
|
|
678
|
+
kind: 'getfacl', operationId: 'ec.fs.getfacl', canonicalArgv: ['fs', 'getfacl', positionals[0], ...(actorAid ? ['--as', actorAid] : [])],
|
|
679
|
+
actorAid, groupId: remote.groupId, remotePath: remote.remotePath,
|
|
680
|
+
} };
|
|
681
|
+
}
|
|
682
|
+
if (rawKind === 'setfacl') {
|
|
683
|
+
const modify = values.get('-m');
|
|
684
|
+
const remove = values.get('-x');
|
|
685
|
+
if (!!modify === !!remove)
|
|
686
|
+
return failFs('INVALID_ARGUMENT', 'setfacl requires exactly one of -m or -x');
|
|
687
|
+
const spec = modify ?? remove ?? '';
|
|
688
|
+
const parsedAcl = parseManagedGroupAclSpec(spec, modify !== undefined);
|
|
689
|
+
if (!('grantee' in parsedAcl))
|
|
690
|
+
return parsedAcl;
|
|
691
|
+
const remote = remoteOrError(positionals[0]);
|
|
692
|
+
if (!('groupId' in remote))
|
|
693
|
+
return remote;
|
|
694
|
+
return { ok: true, command: {
|
|
695
|
+
kind: 'setfacl', operationId: 'ec.fs.setfacl', canonicalArgv: ['fs', 'setfacl', positionals[0], modify !== undefined ? '-m' : '-x', spec, ...(actorAid ? ['--as', actorAid] : [])],
|
|
696
|
+
actorAid, groupId: remote.groupId, remotePath: remote.remotePath,
|
|
697
|
+
aclGrantee: parsedAcl.grantee, ...(parsedAcl.perms ? { aclPerms: parsedAcl.perms } : {}), aclRemove: remove !== undefined,
|
|
698
|
+
} };
|
|
699
|
+
}
|
|
670
700
|
if (rawKind === 'cp') {
|
|
671
701
|
const destination = remoteOrError(positionals[1]);
|
|
672
702
|
if (!('groupId' in destination))
|
|
@@ -735,6 +765,26 @@ export function resolveManagedFsOperation(argv) {
|
|
|
735
765
|
name: values.get('--name') ?? values.get('--pattern'), nodeType, size: values.get('--size'), mtime: values.get('--mtime'), page, pageSize,
|
|
736
766
|
} };
|
|
737
767
|
}
|
|
768
|
+
function parseManagedGroupAclSpec(value, requirePerms) {
|
|
769
|
+
const parts = value.split(':');
|
|
770
|
+
if (parts[0] !== 'role' || !parts[1] || parts.length !== (requirePerms ? 3 : 2)) {
|
|
771
|
+
return failFs('INVALID_ARGUMENT', 'group ACL entry must use role:member:<perms>, role:admin:<perms>, or role:<role>');
|
|
772
|
+
}
|
|
773
|
+
const grantee = `role:${parts[1]}`;
|
|
774
|
+
if (grantee !== 'role:member' && grantee !== 'role:admin') {
|
|
775
|
+
return failFs('INVALID_ARGUMENT', 'group ACL only supports role:member or role:admin');
|
|
776
|
+
}
|
|
777
|
+
if (!requirePerms)
|
|
778
|
+
return { grantee };
|
|
779
|
+
const perms = parts[2];
|
|
780
|
+
if (!perms || !/^(?:r|rw|rwx)$/.test(perms)) {
|
|
781
|
+
return failFs('INVALID_ARGUMENT', 'group ACL permissions must be r, rw, or rwx');
|
|
782
|
+
}
|
|
783
|
+
if (grantee === 'role:member' && perms !== 'rw') {
|
|
784
|
+
return failFs('INVALID_ARGUMENT', 'role:member may only receive rw group ACL permissions');
|
|
785
|
+
}
|
|
786
|
+
return { grantee, perms };
|
|
787
|
+
}
|
|
738
788
|
function numberFrom(value, keys) {
|
|
739
789
|
for (const key of keys) {
|
|
740
790
|
const number = Number(value?.[key]);
|
|
@@ -823,6 +873,16 @@ export async function executeManagedFsOperation(command) {
|
|
|
823
873
|
}
|
|
824
874
|
case 'df':
|
|
825
875
|
return { ok: true, command: 'df', path: `${command.groupId}:`, usage: await client.group.fs.df(`${command.groupId}:`) };
|
|
876
|
+
case 'getfacl':
|
|
877
|
+
return { ok: true, command: 'getfacl', path: ref, result: await client.group.fs.getAcl(ref) };
|
|
878
|
+
case 'setfacl': {
|
|
879
|
+
if (!command.aclGrantee)
|
|
880
|
+
throw new ManagedFsOperationError('INVALID_ACL', 'group ACL grantee is required');
|
|
881
|
+
const result = command.aclRemove
|
|
882
|
+
? await client.group.fs.removeAcl(ref, { granteeAid: command.aclGrantee })
|
|
883
|
+
: await client.group.fs.setAcl(ref, { granteeAid: command.aclGrantee, perms: command.aclPerms });
|
|
884
|
+
return { ok: true, command: 'setfacl', path: ref, grantee: command.aclGrantee, removed: !!command.aclRemove, result };
|
|
885
|
+
}
|
|
826
886
|
case 'cat': {
|
|
827
887
|
const node = await client.group.fs.stat(ref);
|
|
828
888
|
const size = numberFrom(node, ['size', 'size_bytes', 'bytes']);
|