evolcore 0.0.20 → 0.0.22
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 +65 -0
- package/README.md +58 -9
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +393 -108
- package/dist/agents/codex-app-server-client.js +41 -7
- package/dist/agents/codex-runner.js +1292 -220
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +189 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +618 -123
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +98 -123
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +50 -0
- package/dist/cli/trigger-command.js +14 -5
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +19 -5
- package/dist/config/config-manager.js +167 -22
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +43 -1
- package/dist/core/auth/auth-gateway.js +41 -4
- package/dist/core/auth/authorization-audit.js +216 -8
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +99 -27
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +311 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +288 -80
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +377 -36
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +37 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +33 -3
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +90 -87
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +184 -12
- package/dist/core/message/message-log.js +47 -7
- package/dist/core/message/message-queue.js +227 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +658 -109
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +627 -69
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +618 -23
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +329 -30
- package/dist/core/session/session-renew.js +37 -13
- package/dist/core/session/session-turn-coordinator.js +16 -5
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +316 -50
- package/dist/ipc.js +459 -29
- package/dist/paths.js +82 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +70 -7
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +13 -3
- package/dist/trigger/scheduler.js +20 -3
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +30 -6
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +3 -3
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/msg.md +16 -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/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
|
@@ -6,13 +6,16 @@ import { AssistantMessageEventStream } from '../../ecagent/dist/runtime/utils/ev
|
|
|
6
6
|
import { DEFAULT_PERMISSION_MODE } from '../types.js';
|
|
7
7
|
import { agentSessionsDir, resolveRoot } from '../paths.js';
|
|
8
8
|
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
9
|
-
import { normalizePermissionMode } from '../core/permission/mode.js';
|
|
9
|
+
import { normalizeExecutionPermissionMode, 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
13
|
import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
14
14
|
import { resolveEcagentConfig } from './baseagent.js';
|
|
15
15
|
import { buildModelRequestHeaders } from './request-identity.js';
|
|
16
|
+
import { appendRequestPath } from './request-identity.js';
|
|
17
|
+
import { getManagedTaskTempDir } from '../cli/task-context.js';
|
|
18
|
+
import { hasTrustedFullAccessContext } from './runner-types.js';
|
|
16
19
|
const PROVIDER_ID = 'evolcore-gateway';
|
|
17
20
|
const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
|
|
18
21
|
Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
|
|
@@ -248,7 +251,7 @@ function createOpenAiTransport(config) {
|
|
|
248
251
|
const replacement = await options?.onPayload?.(payload, model);
|
|
249
252
|
if (replacement !== undefined)
|
|
250
253
|
payload = replacement;
|
|
251
|
-
response = await fetch(
|
|
254
|
+
response = await fetch(appendRequestPath(model.baseUrl, 'chat/completions', config.queryParams), {
|
|
252
255
|
method: 'POST',
|
|
253
256
|
headers: {
|
|
254
257
|
accept: 'text/event-stream',
|
|
@@ -459,14 +462,14 @@ function isWithin(root, candidate) {
|
|
|
459
462
|
const relative = path.relative(root, candidate);
|
|
460
463
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
461
464
|
}
|
|
462
|
-
export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath) {
|
|
465
|
+
export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath, fullaccess = false) {
|
|
463
466
|
const root = fs.realpathSync(projectPath);
|
|
464
467
|
const resolved = path.resolve(root, candidate || '.');
|
|
465
468
|
const allowedBootstrapFile = bootstrapAgentMdPath
|
|
466
469
|
? path.resolve(bootstrapAgentMdPath)
|
|
467
470
|
: undefined;
|
|
468
471
|
const isBootstrapAgentMd = allowedBootstrapFile === resolved;
|
|
469
|
-
if (!isWithin(root, resolved) && !isBootstrapAgentMd) {
|
|
472
|
+
if (!fullaccess && !isWithin(root, resolved) && !isBootstrapAgentMd) {
|
|
470
473
|
throw new Error(`Path is outside the project: ${candidate}`);
|
|
471
474
|
}
|
|
472
475
|
if (isBootstrapAgentMd) {
|
|
@@ -488,7 +491,7 @@ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false,
|
|
|
488
491
|
while (!fs.existsSync(cursor) && cursor !== path.dirname(cursor))
|
|
489
492
|
cursor = path.dirname(cursor);
|
|
490
493
|
const canonical = fs.realpathSync(cursor);
|
|
491
|
-
if (!isWithin(root, canonical))
|
|
494
|
+
if (!fullaccess && !isWithin(root, canonical))
|
|
492
495
|
throw new Error(`Path resolves outside the project: ${candidate}`);
|
|
493
496
|
return resolved;
|
|
494
497
|
}
|
|
@@ -541,7 +544,7 @@ async function walkFiles(root, signal) {
|
|
|
541
544
|
function textResult(text, details = {}) {
|
|
542
545
|
return { content: [{ type: 'text', text: truncate(text) }], details };
|
|
543
546
|
}
|
|
544
|
-
function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
|
|
547
|
+
function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullaccess = false) {
|
|
545
548
|
const read = {
|
|
546
549
|
name: 'Read', label: 'Read', description: 'Read a UTF-8 text file from the project.',
|
|
547
550
|
parameters: Type.Object({
|
|
@@ -550,7 +553,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
|
|
|
550
553
|
limit: Type.Optional(Type.Number({ minimum: 1 })),
|
|
551
554
|
}),
|
|
552
555
|
async execute(_id, params, signal) {
|
|
553
|
-
const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
|
|
556
|
+
const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess);
|
|
554
557
|
const stat = await fs.promises.stat(file);
|
|
555
558
|
if (stat.size > MAX_READ_BYTES)
|
|
556
559
|
throw new Error(`File exceeds ${MAX_READ_BYTES} byte read limit`);
|
|
@@ -567,7 +570,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
|
|
|
567
570
|
name: 'Glob', label: 'Glob', description: 'Find project files matching a glob pattern.',
|
|
568
571
|
parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }),
|
|
569
572
|
async execute(_id, params, signal) {
|
|
570
|
-
const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
|
|
573
|
+
const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess);
|
|
571
574
|
const matcher = globRegex(params.pattern.replaceAll('\\', '/'));
|
|
572
575
|
const files = (await walkFiles(root, signal))
|
|
573
576
|
.map(file => path.relative(root, file).replaceAll(path.sep, '/'))
|
|
@@ -583,7 +586,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
|
|
|
583
586
|
case_insensitive: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()),
|
|
584
587
|
}),
|
|
585
588
|
async execute(_id, params, signal) {
|
|
586
|
-
const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
|
|
589
|
+
const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess);
|
|
587
590
|
const files = (await fs.promises.stat(root)).isFile() ? [root] : await walkFiles(root, signal);
|
|
588
591
|
const fileMatcher = params.glob ? globRegex(params.glob.replaceAll('\\', '/')) : undefined;
|
|
589
592
|
const flags = params.case_insensitive ? 'i' : '';
|
|
@@ -616,7 +619,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
|
|
|
616
619
|
name: 'Write', label: 'Write', description: 'Create or completely overwrite a project file.',
|
|
617
620
|
parameters: Type.Object({ file_path: Type.String(), content: Type.String() }), executionMode: 'sequential',
|
|
618
621
|
async execute(_id, params, signal) {
|
|
619
|
-
const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath);
|
|
622
|
+
const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess);
|
|
620
623
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
621
624
|
await fs.promises.writeFile(file, params.content, { encoding: 'utf8', signal });
|
|
622
625
|
return textResult(`Wrote ${Buffer.byteLength(params.content)} bytes to ${path.relative(projectPath, file)}`, { file });
|
|
@@ -629,7 +632,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
|
|
|
629
632
|
replace_all: Type.Optional(Type.Boolean()),
|
|
630
633
|
}), executionMode: 'sequential',
|
|
631
634
|
async execute(_id, params, signal) {
|
|
632
|
-
const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
|
|
635
|
+
const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess);
|
|
633
636
|
const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
|
|
634
637
|
if (!params.old_string)
|
|
635
638
|
throw new Error('old_string must not be empty');
|
|
@@ -685,39 +688,43 @@ function errorType(message) {
|
|
|
685
688
|
export class EcagentRunner {
|
|
686
689
|
name = 'ecagent';
|
|
687
690
|
capabilities = {
|
|
691
|
+
fullaccess: true,
|
|
688
692
|
clear: true, compact: true, fork: true, askUserQuestion: false, planApproval: false, fileRewind: 'unsupported',
|
|
689
693
|
};
|
|
690
694
|
config;
|
|
695
|
+
agentAid;
|
|
691
696
|
onSessionIdUpdate;
|
|
692
697
|
model;
|
|
693
698
|
effort;
|
|
694
699
|
permissionMode = DEFAULT_PERMISSION_MODE;
|
|
695
700
|
runtimes = new Map();
|
|
696
701
|
activeStreams = new Map();
|
|
697
|
-
|
|
702
|
+
/** Queue identity prevents an old run's finally from consuming a new run's interrupt. */
|
|
703
|
+
pendingInterrupts = new Map();
|
|
698
704
|
permissionContexts = new Map();
|
|
699
705
|
permissionGateway;
|
|
700
706
|
sendPromptFn;
|
|
701
707
|
onCompactStart;
|
|
702
708
|
modelCache;
|
|
703
|
-
constructor(config, callbacks) {
|
|
709
|
+
constructor(config, callbacks, runtime) {
|
|
704
710
|
this.config = {
|
|
705
711
|
...config,
|
|
706
712
|
headers: buildModelRequestHeaders({
|
|
707
713
|
baseagent: 'ecagent',
|
|
708
714
|
baseUrl: config.baseUrl,
|
|
709
|
-
agentAid:
|
|
715
|
+
agentAid: runtime?.agentAid,
|
|
710
716
|
configuredHeaders: config.headers,
|
|
711
717
|
}),
|
|
712
718
|
};
|
|
713
719
|
this.model = config.model;
|
|
714
720
|
this.effort = config.effort;
|
|
721
|
+
this.agentAid = runtime?.agentAid;
|
|
715
722
|
this.onSessionIdUpdate = callbacks?.onSessionIdUpdate;
|
|
716
723
|
}
|
|
717
724
|
sessionsRoot() {
|
|
718
|
-
if (!this.
|
|
725
|
+
if (!this.agentAid)
|
|
719
726
|
throw new Error('ecagent requires an EvolCore Agent AID');
|
|
720
|
-
return agentSessionsDir(this.
|
|
727
|
+
return agentSessionsDir(this.agentAid);
|
|
721
728
|
}
|
|
722
729
|
setModel(model) { this.model = model; }
|
|
723
730
|
getModel() { return this.model; }
|
|
@@ -744,7 +751,7 @@ export class EcagentRunner {
|
|
|
744
751
|
const stale = this.modelCache?.ids;
|
|
745
752
|
try {
|
|
746
753
|
const signal = AbortSignal.timeout(MODEL_LIST_TIMEOUT_MS);
|
|
747
|
-
const response = await fetch(
|
|
754
|
+
const response = await fetch(appendRequestPath(this.config.baseUrl, 'models', this.config.queryParams), {
|
|
748
755
|
headers: {
|
|
749
756
|
authorization: `Bearer ${this.config.apiKey}`,
|
|
750
757
|
...this.config.headers,
|
|
@@ -769,20 +776,53 @@ export class EcagentRunner {
|
|
|
769
776
|
}
|
|
770
777
|
}
|
|
771
778
|
registerStream(key, stream) { this.activeStreams.set(key, stream); }
|
|
772
|
-
cleanupStream(key) {
|
|
779
|
+
cleanupStream(key) {
|
|
780
|
+
const stream = this.activeStreams.get(key);
|
|
781
|
+
if (!stream)
|
|
782
|
+
return;
|
|
783
|
+
this.activeStreams.delete(key);
|
|
784
|
+
if (this.pendingInterrupts.get(key) === stream)
|
|
785
|
+
this.pendingInterrupts.delete(key);
|
|
786
|
+
}
|
|
773
787
|
hasActiveStream(key) { return this.activeStreams.has(key) || !!this.runtimes.get(key)?.harness; }
|
|
774
788
|
updateSessionId(sessionId, agentSessionId) {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
789
|
+
const previousRuntime = this.runtimes.get(sessionId);
|
|
790
|
+
const applyLocal = () => {
|
|
791
|
+
if (!agentSessionId) {
|
|
792
|
+
if (this.runtimes.get(sessionId) === previousRuntime)
|
|
793
|
+
this.runtimes.delete(sessionId);
|
|
794
|
+
}
|
|
795
|
+
else if (previousRuntime && this.runtimes.get(sessionId) === previousRuntime) {
|
|
796
|
+
previousRuntime.agentSessionId = agentSessionId;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
if (!this.onSessionIdUpdate) {
|
|
800
|
+
applyLocal();
|
|
801
|
+
return;
|
|
782
802
|
}
|
|
783
|
-
|
|
803
|
+
// A discovered non-empty ID is not published to the shared runtime until
|
|
804
|
+
// the authoritative Session activation accepts it. Empty callbacks are
|
|
805
|
+
// local detach notifications and are safe to apply after completion.
|
|
806
|
+
void Promise.resolve(this.onSessionIdUpdate(sessionId, agentSessionId)).then(result => {
|
|
807
|
+
if (result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated' || !agentSessionId) {
|
|
808
|
+
applyLocal();
|
|
809
|
+
}
|
|
810
|
+
}).catch(error => {
|
|
811
|
+
logger.warn(`[EcagentRunner] session binding callback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
812
|
+
if (!agentSessionId)
|
|
813
|
+
applyLocal();
|
|
814
|
+
});
|
|
784
815
|
}
|
|
785
816
|
async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, _sessionManager, modelOverride, runtimeEnv = {}) {
|
|
817
|
+
// Keep managed tasks from silently inheriting the daemon-wide TMPDIR when
|
|
818
|
+
// their session-owned runtime directory is unavailable.
|
|
819
|
+
getManagedTaskTempDir(runtimeEnv);
|
|
820
|
+
const topicBinding = modelOverride?.backend?.kind === 'topic';
|
|
821
|
+
if (topicBinding && (!modelOverride?.turn || modelOverride.turn.sessionId !== sessionId || !this.onSessionIdUpdate)) {
|
|
822
|
+
const error = new Error('topic backend run requires an active TurnLease and binding callback');
|
|
823
|
+
error.code = 'TOPIC_TURN_LEASE_REQUIRED';
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
786
826
|
const queue = new AsyncEventQueue();
|
|
787
827
|
this.activeStreams.set(sessionId, queue);
|
|
788
828
|
void this.executeRun(queue, {
|
|
@@ -797,15 +837,19 @@ export class EcagentRunner {
|
|
|
797
837
|
try {
|
|
798
838
|
await fs.promises.mkdir(request.projectPath, { recursive: true });
|
|
799
839
|
const previousRuntime = this.runtimes.get(request.sessionId);
|
|
800
|
-
const runtime = await this.getOrCreateRuntime(request.sessionId, request.projectPath, request.initialAgentSessionId);
|
|
801
|
-
if (
|
|
840
|
+
const runtime = await this.getOrCreateRuntime(request.sessionId, request.projectPath, request.initialAgentSessionId, request.modelOverride);
|
|
841
|
+
if (previousRuntime !== runtime) {
|
|
802
842
|
queue.push({ type: 'session_id', sessionId: runtime.agentSessionId });
|
|
803
843
|
}
|
|
804
844
|
const modelId = request.modelOverride?.model || this.model;
|
|
805
845
|
const effort = request.modelOverride?.effortMode === 'model_default'
|
|
806
846
|
? undefined
|
|
807
847
|
: request.modelOverride?.effort ?? this.effort;
|
|
808
|
-
const permissionMode =
|
|
848
|
+
const permissionMode = normalizeExecutionPermissionMode(request.modelOverride?.permissionMode || this.permissionMode);
|
|
849
|
+
const fullaccess = permissionMode === 'fullaccess';
|
|
850
|
+
if (fullaccess && !hasTrustedFullAccessContext(this.permissionContexts.get(request.sessionId))) {
|
|
851
|
+
throw new Error('Ecagent fullaccess requires a trusted per-call execution authorization');
|
|
852
|
+
}
|
|
809
853
|
const model = createModel(modelId, this.config.baseUrl);
|
|
810
854
|
const nodeEnv = new NodeExecutionEnv({ cwd: request.projectPath, shellEnv: { ...process.env, ...request.runtimeEnv, EVOLCORE_SESSION_ID: request.sessionId, EVOLCORE_HOME: resolveRoot() } });
|
|
811
855
|
const models = createModels();
|
|
@@ -828,7 +872,7 @@ export class EcagentRunner {
|
|
|
828
872
|
models,
|
|
829
873
|
model,
|
|
830
874
|
thinkingLevel: normalizeThinkingLevel(effort),
|
|
831
|
-
tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath),
|
|
875
|
+
tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath, fullaccess),
|
|
832
876
|
systemPrompt: [DEFAULT_SYSTEM_PROMPT, request.systemPromptAppend].filter(Boolean).join('\n\n'),
|
|
833
877
|
streamOptions: {
|
|
834
878
|
timeoutMs: this.config.timeoutMs,
|
|
@@ -837,7 +881,8 @@ export class EcagentRunner {
|
|
|
837
881
|
},
|
|
838
882
|
});
|
|
839
883
|
runtime.harness = harness;
|
|
840
|
-
if (this.pendingInterrupts.
|
|
884
|
+
if (this.pendingInterrupts.get(request.sessionId) === queue) {
|
|
885
|
+
this.pendingInterrupts.delete(request.sessionId);
|
|
841
886
|
await harness.abort();
|
|
842
887
|
runtime.harness = undefined;
|
|
843
888
|
throw new Error('ecagent run interrupted');
|
|
@@ -903,15 +948,28 @@ export class EcagentRunner {
|
|
|
903
948
|
finally {
|
|
904
949
|
if (!completeSent)
|
|
905
950
|
queue.push({ type: 'complete', isError: true, terminalReason: 'error', durationMs: Date.now() - stats.startedAt });
|
|
906
|
-
this.
|
|
907
|
-
|
|
951
|
+
if (this.activeStreams.get(request.sessionId) === queue) {
|
|
952
|
+
this.activeStreams.delete(request.sessionId);
|
|
953
|
+
if (this.pendingInterrupts.get(request.sessionId) === queue) {
|
|
954
|
+
this.pendingInterrupts.delete(request.sessionId);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
908
957
|
queue.end();
|
|
909
958
|
}
|
|
910
959
|
}
|
|
911
|
-
async getOrCreateRuntime(sessionId, projectPath, requestedId) {
|
|
960
|
+
async getOrCreateRuntime(sessionId, projectPath, requestedId, modelOverride) {
|
|
912
961
|
const current = this.runtimes.get(sessionId);
|
|
913
|
-
const
|
|
914
|
-
|
|
962
|
+
const topicBinding = modelOverride?.backend?.kind === 'topic';
|
|
963
|
+
const requestedTopicAgentSessionId = topicBinding
|
|
964
|
+
? modelOverride?.backend?.agentSessionId ?? null
|
|
965
|
+
: undefined;
|
|
966
|
+
const targetId = topicBinding
|
|
967
|
+
? (requestedTopicAgentSessionId || undefined)
|
|
968
|
+
: requestedId || current?.agentSessionId;
|
|
969
|
+
if (current
|
|
970
|
+
&& current.projectPath === projectPath
|
|
971
|
+
&& (!targetId || current.agentSessionId === targetId)
|
|
972
|
+
&& (!topicBinding || (!!targetId && current.agentSessionId === targetId)))
|
|
915
973
|
return current;
|
|
916
974
|
const env = new NodeExecutionEnv({ cwd: projectPath });
|
|
917
975
|
const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
|
|
@@ -919,21 +977,42 @@ export class EcagentRunner {
|
|
|
919
977
|
if (targetId) {
|
|
920
978
|
const metadata = (await repo.list({ cwd: projectPath })).find(item => item.id === targetId);
|
|
921
979
|
if (!metadata) {
|
|
980
|
+
if (topicBinding) {
|
|
981
|
+
const error = new Error(`bound topic backend is unavailable: ${targetId}`);
|
|
982
|
+
error.code = 'TOPIC_BACKEND_UNAVAILABLE';
|
|
983
|
+
throw error;
|
|
984
|
+
}
|
|
922
985
|
logger.warn(`[EcagentRunner] backing session ${targetId} was not found; creating a new session`);
|
|
923
986
|
session = await repo.create({ cwd: projectPath });
|
|
924
987
|
}
|
|
925
988
|
else
|
|
926
989
|
session = await repo.open(metadata);
|
|
927
990
|
}
|
|
928
|
-
else
|
|
929
|
-
|
|
991
|
+
else {
|
|
992
|
+
// Topic UNBOUND is an explicit backend boundary. Let ecagent allocate a
|
|
993
|
+
// fresh provider ID; reusing the EvolCore Session ID would make every
|
|
994
|
+
// /renew cycle reopen the same provider session under a new filename.
|
|
995
|
+
session = topicBinding
|
|
996
|
+
? await repo.create({ cwd: projectPath })
|
|
997
|
+
: await repo.create({ cwd: projectPath, id: sessionId });
|
|
998
|
+
}
|
|
930
999
|
const metadata = await session.getMetadata();
|
|
931
1000
|
const runtime = { agentSessionId: metadata.id, projectPath, repo, session };
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1001
|
+
if ((topicBinding && metadata.id !== requestedTopicAgentSessionId) || metadata.id !== targetId) {
|
|
1002
|
+
const turn = modelOverride?.turn;
|
|
1003
|
+
const result = this.onSessionIdUpdate
|
|
1004
|
+
? (turn === undefined
|
|
1005
|
+
? await this.onSessionIdUpdate(sessionId, metadata.id)
|
|
1006
|
+
: await this.onSessionIdUpdate(sessionId, metadata.id, { turn }))
|
|
1007
|
+
: 'legacy_updated';
|
|
1008
|
+
if (result !== undefined && result !== 'activated' && result !== 'already_active' && result !== 'legacy_updated') {
|
|
1009
|
+
const error = new Error(`backend discovery rejected: ${result}`);
|
|
1010
|
+
error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
|
|
1011
|
+
throw error;
|
|
1012
|
+
}
|
|
935
1013
|
// The public event is emitted by the active query immediately after creation.
|
|
936
1014
|
}
|
|
1015
|
+
this.runtimes.set(sessionId, runtime);
|
|
937
1016
|
return runtime;
|
|
938
1017
|
}
|
|
939
1018
|
mapHarnessEvent(queue, event, stats, model, effort) {
|
|
@@ -987,6 +1066,11 @@ export class EcagentRunner {
|
|
|
987
1066
|
}
|
|
988
1067
|
async authorizeTool(sessionId, projectPath, mode, toolName, input) {
|
|
989
1068
|
const permissionContext = this.permissionContexts.get(sessionId);
|
|
1069
|
+
if (mode === 'fullaccess') {
|
|
1070
|
+
return hasTrustedFullAccessContext(permissionContext)
|
|
1071
|
+
? {}
|
|
1072
|
+
: { block: true, reason: 'Untrusted fullaccess execution context' };
|
|
1073
|
+
}
|
|
990
1074
|
const recordBlockedOperation = async (policyCode, operationInput = input, matchedPath) => {
|
|
991
1075
|
const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
|
|
992
1076
|
auditToolPreflightDenial({
|
|
@@ -996,6 +1080,7 @@ export class EcagentRunner {
|
|
|
996
1080
|
summary,
|
|
997
1081
|
sessionId,
|
|
998
1082
|
agentAid: permissionContext?.selfAid,
|
|
1083
|
+
agentName: permissionContext?.agentName,
|
|
999
1084
|
permissionMode: mode,
|
|
1000
1085
|
channel: permissionContext?.channel,
|
|
1001
1086
|
actorId: permissionContext?.userId,
|
|
@@ -1014,6 +1099,7 @@ export class EcagentRunner {
|
|
|
1014
1099
|
policyCode,
|
|
1015
1100
|
decisionSource: 'policy',
|
|
1016
1101
|
agentAid: permissionContext?.selfAid,
|
|
1102
|
+
agentName: permissionContext?.agentName,
|
|
1017
1103
|
permissionMode: mode,
|
|
1018
1104
|
summary,
|
|
1019
1105
|
effect: 'operation_skipped',
|
|
@@ -1030,6 +1116,7 @@ export class EcagentRunner {
|
|
|
1030
1116
|
}
|
|
1031
1117
|
const preflight = evaluateToolPreflight(toolName, input, {
|
|
1032
1118
|
sessionId,
|
|
1119
|
+
managedTempDir: permissionContext?.managedTempDir,
|
|
1033
1120
|
selfAid: permissionContext?.selfAid,
|
|
1034
1121
|
channel: permissionContext?.channel,
|
|
1035
1122
|
userId: permissionContext?.userId,
|
|
@@ -1049,7 +1136,7 @@ export class EcagentRunner {
|
|
|
1049
1136
|
const checkedInput = preflight.input;
|
|
1050
1137
|
if (mode === 'readonly') {
|
|
1051
1138
|
const decision = checkReadonly(toolName, checkedInput, projectPath, {
|
|
1052
|
-
sessionId, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
|
|
1139
|
+
sessionId, managedTempDir: permissionContext?.managedTempDir, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
|
|
1053
1140
|
allowReadonlySourceDiagnostics: permissionContext?.allowReadonlySourceDiagnostics === true,
|
|
1054
1141
|
});
|
|
1055
1142
|
if (decision.behavior === 'deny') {
|
|
@@ -1078,8 +1165,9 @@ export class EcagentRunner {
|
|
|
1078
1165
|
await harness.abort();
|
|
1079
1166
|
return;
|
|
1080
1167
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1168
|
+
const stream = this.activeStreams.get(sessionKey);
|
|
1169
|
+
if (stream)
|
|
1170
|
+
this.pendingInterrupts.set(sessionKey, stream);
|
|
1083
1171
|
}
|
|
1084
1172
|
injectUserMessage(sessionId, text) {
|
|
1085
1173
|
const harness = this.runtimes.get(sessionId)?.harness;
|
|
@@ -1087,22 +1175,33 @@ export class EcagentRunner {
|
|
|
1087
1175
|
void harness.steer(text).catch(error => logger.warn('[EcagentRunner] Failed to steer active run:', error));
|
|
1088
1176
|
}
|
|
1089
1177
|
async closeSession(sessionId) {
|
|
1178
|
+
const capturedPermissionContext = this.permissionContexts.get(sessionId);
|
|
1090
1179
|
this.permissionContexts.get(sessionId)?.pauseController?.cancel();
|
|
1180
|
+
const runtime = this.runtimes.get(sessionId);
|
|
1181
|
+
const stream = this.activeStreams.get(sessionId);
|
|
1091
1182
|
await this.interrupt(sessionId).catch(() => { });
|
|
1092
|
-
this.runtimes.
|
|
1093
|
-
|
|
1094
|
-
this.
|
|
1095
|
-
|
|
1183
|
+
if (runtime && this.runtimes.get(sessionId) === runtime)
|
|
1184
|
+
this.runtimes.delete(sessionId);
|
|
1185
|
+
if (stream && this.activeStreams.get(sessionId) === stream) {
|
|
1186
|
+
this.activeStreams.delete(sessionId);
|
|
1187
|
+
if (this.pendingInterrupts.get(sessionId) === stream)
|
|
1188
|
+
this.pendingInterrupts.delete(sessionId);
|
|
1189
|
+
}
|
|
1190
|
+
if (this.permissionContexts.get(sessionId) === capturedPermissionContext)
|
|
1191
|
+
this.permissionContexts.delete(sessionId);
|
|
1096
1192
|
}
|
|
1097
1193
|
async clearSession(sessionId, agentSessionId, projectPath) {
|
|
1098
1194
|
try {
|
|
1195
|
+
const currentRuntime = this.runtimes.get(sessionId);
|
|
1099
1196
|
await this.interrupt(sessionId);
|
|
1100
1197
|
const env = new NodeExecutionEnv({ cwd: projectPath });
|
|
1101
1198
|
const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
|
|
1102
1199
|
const metadata = (await repo.list({ cwd: path.resolve(projectPath) })).find(item => item.id === agentSessionId);
|
|
1103
1200
|
if (metadata)
|
|
1104
1201
|
await repo.delete(metadata);
|
|
1105
|
-
|
|
1202
|
+
if (currentRuntime?.agentSessionId === agentSessionId
|
|
1203
|
+
&& this.runtimes.get(sessionId) === currentRuntime)
|
|
1204
|
+
this.runtimes.delete(sessionId);
|
|
1106
1205
|
await this.onSessionIdUpdate?.(sessionId, '');
|
|
1107
1206
|
return !!metadata;
|
|
1108
1207
|
}
|
|
@@ -1111,11 +1210,12 @@ export class EcagentRunner {
|
|
|
1111
1210
|
return false;
|
|
1112
1211
|
}
|
|
1113
1212
|
}
|
|
1114
|
-
async compactSession(sessionId, agentSessionId, projectPath) {
|
|
1213
|
+
async compactSession(sessionId, agentSessionId, projectPath, modelOverride) {
|
|
1214
|
+
const startedAt = Date.now();
|
|
1115
1215
|
try {
|
|
1116
1216
|
if (this.activeStreams.has(sessionId) || this.runtimes.get(sessionId)?.harness) {
|
|
1117
1217
|
logger.warn(`[EcagentRunner] compact skipped while session ${sessionId} is active`);
|
|
1118
|
-
return false;
|
|
1218
|
+
return { ok: false, code: 'invalid_state', message: 'Session is active', durationMs: Date.now() - startedAt };
|
|
1119
1219
|
}
|
|
1120
1220
|
const cwd = path.resolve(projectPath);
|
|
1121
1221
|
const cached = this.runtimes.get(sessionId);
|
|
@@ -1123,7 +1223,11 @@ export class EcagentRunner {
|
|
|
1123
1223
|
? cached
|
|
1124
1224
|
: await this.openExistingRuntime(agentSessionId, cwd);
|
|
1125
1225
|
this.onCompactStart?.(sessionId);
|
|
1126
|
-
const
|
|
1226
|
+
const compactModel = modelOverride?.model || this.model;
|
|
1227
|
+
const compactEffort = modelOverride?.effortMode === 'model_default'
|
|
1228
|
+
? undefined
|
|
1229
|
+
: modelOverride?.effort ?? this.effort;
|
|
1230
|
+
const model = createModel(compactModel, this.config.baseUrl);
|
|
1127
1231
|
const env = new NodeExecutionEnv({ cwd: runtime.projectPath });
|
|
1128
1232
|
const models = createModels();
|
|
1129
1233
|
const transport = createOpenAiTransport(this.config);
|
|
@@ -1131,7 +1235,7 @@ export class EcagentRunner {
|
|
|
1131
1235
|
id: PROVIDER_ID, auth: { apiKey: { name: 'EvolCore gateway', login: async () => ({ type: 'api_key', key: this.config.apiKey }), resolve: async () => ({ auth: { apiKey: this.config.apiKey } }) } },
|
|
1132
1236
|
models: [model], api: transport,
|
|
1133
1237
|
}));
|
|
1134
|
-
const harness = new AgentHarness({ env, session: runtime.session, models, model, thinkingLevel: normalizeThinkingLevel(
|
|
1238
|
+
const harness = new AgentHarness({ env, session: runtime.session, models, model, thinkingLevel: normalizeThinkingLevel(compactEffort), tools: [] });
|
|
1135
1239
|
runtime.harness = harness;
|
|
1136
1240
|
try {
|
|
1137
1241
|
await harness.compact();
|
|
@@ -1139,16 +1243,22 @@ export class EcagentRunner {
|
|
|
1139
1243
|
finally {
|
|
1140
1244
|
runtime.harness = undefined;
|
|
1141
1245
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1246
|
+
// Do not let an older compact operation overwrite a runtime that was
|
|
1247
|
+
// published by a later topic turn.
|
|
1248
|
+
if (this.runtimes.get(sessionId) === cached) {
|
|
1249
|
+
this.runtimes.set(sessionId, runtime);
|
|
1250
|
+
}
|
|
1251
|
+
return { ok: true, durationMs: Date.now() - startedAt };
|
|
1144
1252
|
}
|
|
1145
1253
|
catch (error) {
|
|
1146
|
-
|
|
1147
|
-
|
|
1254
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1255
|
+
const code = /aborted/i.test(message) ? 'aborted' : /invalid_state/i.test(message) ? 'invalid_state' : 'sdk_error';
|
|
1256
|
+
logger.error(`[EcagentRunner] compact failed: session=${sessionId} agent=${agentSessionId} code=${code} durationMs=${Date.now() - startedAt} error=${message}`);
|
|
1257
|
+
return { ok: false, code, message, durationMs: Date.now() - startedAt };
|
|
1148
1258
|
}
|
|
1149
1259
|
}
|
|
1150
|
-
compact(sessionId, agentSessionId, projectPath) {
|
|
1151
|
-
return this.compactSession(sessionId, agentSessionId, projectPath);
|
|
1260
|
+
compact(sessionId, agentSessionId, projectPath, modelOverride) {
|
|
1261
|
+
return this.compactSession(sessionId, agentSessionId, projectPath, modelOverride);
|
|
1152
1262
|
}
|
|
1153
1263
|
resolveSessionFile(agentSessionId, projectPath) {
|
|
1154
1264
|
const dir = path.join(this.sessionsRoot(), `--${path.resolve(projectPath).replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`);
|
|
@@ -1251,8 +1361,8 @@ export class EcagentAgentPlugin {
|
|
|
1251
1361
|
const override = agent.config.baseagents?.ecagent;
|
|
1252
1362
|
if (!override)
|
|
1253
1363
|
return null;
|
|
1254
|
-
const configured = { ...override
|
|
1364
|
+
const configured = { ...override };
|
|
1255
1365
|
const resolved = resolveEcagentConfig({ agents: { ecagent: configured } }, configured);
|
|
1256
|
-
return { evolagentName: agent.name, baseagent: 'ecagent', agent: new EcagentRunner(resolved, callbacks) };
|
|
1366
|
+
return { evolagentName: agent.name, baseagent: 'ecagent', agent: new EcagentRunner(resolved, callbacks, { agentAid: agent.aid }) };
|
|
1257
1367
|
}
|
|
1258
1368
|
}
|