evolcore 0.0.19 → 0.0.21
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 +43 -1
- package/README.md +60 -9
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +383 -111
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +405 -137
- package/dist/agents/ecagent-runner.js +174 -63
- 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 +66 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +86 -9
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +26 -11
- package/dist/aun/outbox.js +295 -68
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +1004 -273
- 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 +81 -121
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +76 -24
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +226 -24
- package/dist/config/config-operation-service.js +1 -2
- 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 +29 -16
- package/dist/config/lifecycle.js +16 -5
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +161 -12
- 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 +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +155 -4
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +27 -38
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -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 +279 -80
- 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 +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +9 -4
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +495 -72
- 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 +465 -56
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +205 -13
- package/dist/core/permission/tool-policy.js +673 -62
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +18 -9
- package/dist/index.js +283 -65
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- 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 +21 -2
- 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/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +10 -5
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +421 -0
- package/kits/schemas/daemon.schema.5.json +135 -0
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +119 -0
- 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/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +7 -3
- package/skills/eclink/SKILL.md +2 -0
|
@@ -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,7 +1066,12 @@ export class EcagentRunner {
|
|
|
987
1066
|
}
|
|
988
1067
|
async authorizeTool(sessionId, projectPath, mode, toolName, input) {
|
|
989
1068
|
const permissionContext = this.permissionContexts.get(sessionId);
|
|
990
|
-
|
|
1069
|
+
if (mode === 'fullaccess') {
|
|
1070
|
+
return hasTrustedFullAccessContext(permissionContext)
|
|
1071
|
+
? {}
|
|
1072
|
+
: { block: true, reason: 'Untrusted fullaccess execution context' };
|
|
1073
|
+
}
|
|
1074
|
+
const recordBlockedOperation = async (policyCode, operationInput = input, matchedPath) => {
|
|
991
1075
|
const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
|
|
992
1076
|
auditToolPreflightDenial({
|
|
993
1077
|
toolName,
|
|
@@ -996,12 +1080,14 @@ 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,
|
|
1002
1087
|
role: permissionContext?.role,
|
|
1003
1088
|
selfAid: permissionContext?.selfAid,
|
|
1004
1089
|
taskId: permissionContext?.taskId,
|
|
1090
|
+
matchedPath,
|
|
1005
1091
|
});
|
|
1006
1092
|
try {
|
|
1007
1093
|
await permissionContext?.recordExecutionAnomaly?.({
|
|
@@ -1013,6 +1099,7 @@ export class EcagentRunner {
|
|
|
1013
1099
|
policyCode,
|
|
1014
1100
|
decisionSource: 'policy',
|
|
1015
1101
|
agentAid: permissionContext?.selfAid,
|
|
1102
|
+
agentName: permissionContext?.agentName,
|
|
1016
1103
|
permissionMode: mode,
|
|
1017
1104
|
summary,
|
|
1018
1105
|
effect: 'operation_skipped',
|
|
@@ -1029,6 +1116,7 @@ export class EcagentRunner {
|
|
|
1029
1116
|
}
|
|
1030
1117
|
const preflight = evaluateToolPreflight(toolName, input, {
|
|
1031
1118
|
sessionId,
|
|
1119
|
+
managedTempDir: permissionContext?.managedTempDir,
|
|
1032
1120
|
selfAid: permissionContext?.selfAid,
|
|
1033
1121
|
channel: permissionContext?.channel,
|
|
1034
1122
|
userId: permissionContext?.userId,
|
|
@@ -1039,7 +1127,7 @@ export class EcagentRunner {
|
|
|
1039
1127
|
});
|
|
1040
1128
|
if (preflight.behavior === 'deny') {
|
|
1041
1129
|
if (preflight.policyCode) {
|
|
1042
|
-
await recordBlockedOperation(preflight.policyCode, input);
|
|
1130
|
+
await recordBlockedOperation(preflight.policyCode, input, preflight.matchedPath);
|
|
1043
1131
|
}
|
|
1044
1132
|
return { block: true, reason: preflight.message };
|
|
1045
1133
|
}
|
|
@@ -1048,7 +1136,7 @@ export class EcagentRunner {
|
|
|
1048
1136
|
const checkedInput = preflight.input;
|
|
1049
1137
|
if (mode === 'readonly') {
|
|
1050
1138
|
const decision = checkReadonly(toolName, checkedInput, projectPath, {
|
|
1051
|
-
sessionId, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
|
|
1139
|
+
sessionId, managedTempDir: permissionContext?.managedTempDir, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
|
|
1052
1140
|
allowReadonlySourceDiagnostics: permissionContext?.allowReadonlySourceDiagnostics === true,
|
|
1053
1141
|
});
|
|
1054
1142
|
if (decision.behavior === 'deny') {
|
|
@@ -1077,8 +1165,9 @@ export class EcagentRunner {
|
|
|
1077
1165
|
await harness.abort();
|
|
1078
1166
|
return;
|
|
1079
1167
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1168
|
+
const stream = this.activeStreams.get(sessionKey);
|
|
1169
|
+
if (stream)
|
|
1170
|
+
this.pendingInterrupts.set(sessionKey, stream);
|
|
1082
1171
|
}
|
|
1083
1172
|
injectUserMessage(sessionId, text) {
|
|
1084
1173
|
const harness = this.runtimes.get(sessionId)?.harness;
|
|
@@ -1086,22 +1175,33 @@ export class EcagentRunner {
|
|
|
1086
1175
|
void harness.steer(text).catch(error => logger.warn('[EcagentRunner] Failed to steer active run:', error));
|
|
1087
1176
|
}
|
|
1088
1177
|
async closeSession(sessionId) {
|
|
1178
|
+
const capturedPermissionContext = this.permissionContexts.get(sessionId);
|
|
1089
1179
|
this.permissionContexts.get(sessionId)?.pauseController?.cancel();
|
|
1180
|
+
const runtime = this.runtimes.get(sessionId);
|
|
1181
|
+
const stream = this.activeStreams.get(sessionId);
|
|
1090
1182
|
await this.interrupt(sessionId).catch(() => { });
|
|
1091
|
-
this.runtimes.
|
|
1092
|
-
|
|
1093
|
-
this.
|
|
1094
|
-
|
|
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);
|
|
1095
1192
|
}
|
|
1096
1193
|
async clearSession(sessionId, agentSessionId, projectPath) {
|
|
1097
1194
|
try {
|
|
1195
|
+
const currentRuntime = this.runtimes.get(sessionId);
|
|
1098
1196
|
await this.interrupt(sessionId);
|
|
1099
1197
|
const env = new NodeExecutionEnv({ cwd: projectPath });
|
|
1100
1198
|
const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: this.sessionsRoot() });
|
|
1101
1199
|
const metadata = (await repo.list({ cwd: path.resolve(projectPath) })).find(item => item.id === agentSessionId);
|
|
1102
1200
|
if (metadata)
|
|
1103
1201
|
await repo.delete(metadata);
|
|
1104
|
-
|
|
1202
|
+
if (currentRuntime?.agentSessionId === agentSessionId
|
|
1203
|
+
&& this.runtimes.get(sessionId) === currentRuntime)
|
|
1204
|
+
this.runtimes.delete(sessionId);
|
|
1105
1205
|
await this.onSessionIdUpdate?.(sessionId, '');
|
|
1106
1206
|
return !!metadata;
|
|
1107
1207
|
}
|
|
@@ -1110,11 +1210,12 @@ export class EcagentRunner {
|
|
|
1110
1210
|
return false;
|
|
1111
1211
|
}
|
|
1112
1212
|
}
|
|
1113
|
-
async compactSession(sessionId, agentSessionId, projectPath) {
|
|
1213
|
+
async compactSession(sessionId, agentSessionId, projectPath, modelOverride) {
|
|
1214
|
+
const startedAt = Date.now();
|
|
1114
1215
|
try {
|
|
1115
1216
|
if (this.activeStreams.has(sessionId) || this.runtimes.get(sessionId)?.harness) {
|
|
1116
1217
|
logger.warn(`[EcagentRunner] compact skipped while session ${sessionId} is active`);
|
|
1117
|
-
return false;
|
|
1218
|
+
return { ok: false, code: 'invalid_state', message: 'Session is active', durationMs: Date.now() - startedAt };
|
|
1118
1219
|
}
|
|
1119
1220
|
const cwd = path.resolve(projectPath);
|
|
1120
1221
|
const cached = this.runtimes.get(sessionId);
|
|
@@ -1122,7 +1223,11 @@ export class EcagentRunner {
|
|
|
1122
1223
|
? cached
|
|
1123
1224
|
: await this.openExistingRuntime(agentSessionId, cwd);
|
|
1124
1225
|
this.onCompactStart?.(sessionId);
|
|
1125
|
-
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);
|
|
1126
1231
|
const env = new NodeExecutionEnv({ cwd: runtime.projectPath });
|
|
1127
1232
|
const models = createModels();
|
|
1128
1233
|
const transport = createOpenAiTransport(this.config);
|
|
@@ -1130,7 +1235,7 @@ export class EcagentRunner {
|
|
|
1130
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 } }) } },
|
|
1131
1236
|
models: [model], api: transport,
|
|
1132
1237
|
}));
|
|
1133
|
-
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: [] });
|
|
1134
1239
|
runtime.harness = harness;
|
|
1135
1240
|
try {
|
|
1136
1241
|
await harness.compact();
|
|
@@ -1138,16 +1243,22 @@ export class EcagentRunner {
|
|
|
1138
1243
|
finally {
|
|
1139
1244
|
runtime.harness = undefined;
|
|
1140
1245
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
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 };
|
|
1143
1252
|
}
|
|
1144
1253
|
catch (error) {
|
|
1145
|
-
|
|
1146
|
-
|
|
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 };
|
|
1147
1258
|
}
|
|
1148
1259
|
}
|
|
1149
|
-
compact(sessionId, agentSessionId, projectPath) {
|
|
1150
|
-
return this.compactSession(sessionId, agentSessionId, projectPath);
|
|
1260
|
+
compact(sessionId, agentSessionId, projectPath, modelOverride) {
|
|
1261
|
+
return this.compactSession(sessionId, agentSessionId, projectPath, modelOverride);
|
|
1151
1262
|
}
|
|
1152
1263
|
resolveSessionFile(agentSessionId, projectPath) {
|
|
1153
1264
|
const dir = path.join(this.sessionsRoot(), `--${path.resolve(projectPath).replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`);
|
|
@@ -1250,8 +1361,8 @@ export class EcagentAgentPlugin {
|
|
|
1250
1361
|
const override = agent.config.baseagents?.ecagent;
|
|
1251
1362
|
if (!override)
|
|
1252
1363
|
return null;
|
|
1253
|
-
const configured = { ...override
|
|
1364
|
+
const configured = { ...override };
|
|
1254
1365
|
const resolved = resolveEcagentConfig({ agents: { ecagent: configured } }, configured);
|
|
1255
|
-
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 }) };
|
|
1256
1367
|
}
|
|
1257
1368
|
}
|