@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.2
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/dist/agent/agent-definition-registry.d.ts +18 -3
- package/dist/agent/agent-definition-registry.js +47 -18
- package/dist/agent/agent-definition.d.ts +17 -0
- package/dist/agent/agent-definition.js +22 -1
- package/dist/arena/arena.js +11 -12
- package/dist/arena/context/context-tools.js +2 -4
- package/dist/arena/context/within-root.d.ts +7 -0
- package/dist/arena/context/within-root.js +15 -0
- package/dist/arena/iterate/phases/argue.js +8 -2
- package/dist/arena/iterate/phases/revise.js +1 -1
- package/dist/arena/iterate/phases/tournament.js +3 -3
- package/dist/arena/phases/adjudication.js +1 -4
- package/dist/arena/phases/build-consensus.js +1 -4
- package/dist/arena/phases/cross-review.js +2 -8
- package/dist/arena/phases/debate-rounds.js +1 -4
- package/dist/arena/phases/participant-research.js +1 -4
- package/dist/arena/phases/planning-detail-expansion.js +8 -10
- package/dist/arena/planner.js +0 -1
- package/dist/arena/providers/docs.d.ts +1 -0
- package/dist/arena/providers/docs.js +5 -2
- package/dist/arena/providers/repo.d.ts +1 -0
- package/dist/arena/providers/repo.js +5 -2
- package/dist/arena/strategies/utils.js +36 -3
- package/dist/arena/transitions.d.ts +7 -2
- package/dist/arena/transitions.js +9 -4
- package/dist/arena/types.d.ts +8 -2
- package/dist/automation/cron-expr.d.ts +31 -0
- package/dist/automation/cron-expr.js +151 -0
- package/dist/automation/index.d.ts +41 -0
- package/dist/automation/index.js +39 -0
- package/dist/automation/runner.d.ts +67 -0
- package/dist/automation/runner.js +56 -0
- package/dist/automation/scheduler.d.ts +156 -0
- package/dist/automation/scheduler.js +546 -0
- package/dist/automation/store.d.ts +39 -0
- package/dist/automation/store.js +119 -0
- package/dist/automation/write-policy.d.ts +34 -0
- package/dist/automation/write-policy.js +113 -0
- package/dist/automation/write-run.d.ts +51 -0
- package/dist/automation/write-run.js +38 -0
- package/dist/capability-control/index.d.ts +5 -0
- package/dist/capability-control/index.js +3 -0
- package/dist/capability-control/overlay.d.ts +49 -0
- package/dist/capability-control/overlay.js +89 -0
- package/dist/capability-control/project.d.ts +65 -0
- package/dist/capability-control/project.js +147 -0
- package/dist/capability-control/service.d.ts +70 -0
- package/dist/capability-control/service.js +147 -0
- package/dist/capability-control/types.d.ts +62 -0
- package/dist/capability-control/types.js +17 -0
- package/dist/cli/agent-server-stdio.d.ts +12 -1
- package/dist/cli/agent-server-stdio.js +114 -19
- package/dist/cli/agent-server-tcp.d.ts +20 -0
- package/dist/cli/agent-server-tcp.js +120 -0
- package/dist/cli/graceful-shutdown.d.ts +27 -0
- package/dist/cli/graceful-shutdown.js +30 -0
- package/dist/context/compaction.js +23 -15
- package/dist/context/manager.d.ts +0 -20
- package/dist/context/manager.js +0 -37
- package/dist/cron/cron-runtime.d.ts +2 -0
- package/dist/cron/cron-runtime.js +2 -0
- package/dist/cron/cron-store.d.ts +2 -0
- package/dist/cron/cron-store.js +2 -0
- package/dist/cron/scheduler.d.ts +5 -32
- package/dist/cron/scheduler.js +5 -113
- package/dist/data/openrouter-sync.js +1 -1
- package/dist/engine/engine.d.ts +208 -14
- package/dist/engine/engine.js +754 -169
- package/dist/engine/goal.d.ts +42 -0
- package/dist/engine/goal.js +52 -0
- package/dist/engine/image-compression.d.ts +81 -0
- package/dist/engine/image-compression.js +186 -0
- package/dist/engine/image-policy.d.ts +152 -0
- package/dist/engine/image-policy.js +165 -0
- package/dist/engine/reactive-threshold.d.ts +13 -0
- package/dist/engine/reactive-threshold.js +17 -0
- package/dist/engine/runtime.js +7 -0
- package/dist/engine/session-title.d.ts +13 -0
- package/dist/engine/session-title.js +34 -0
- package/dist/engine/turn-loop.d.ts +29 -8
- package/dist/engine/turn-loop.js +498 -316
- package/dist/git/parse-log.d.ts +13 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +8 -6
- package/dist/git/utils.js +57 -26
- package/dist/git/worktree.js +33 -18
- package/dist/hooks/events.d.ts +23 -1
- package/dist/hooks/goal-stop-hook.d.ts +30 -0
- package/dist/hooks/goal-stop-hook.js +102 -0
- package/dist/hooks/hook-output.d.ts +33 -0
- package/dist/hooks/hook-output.js +79 -0
- package/dist/hooks/registry.d.ts +7 -0
- package/dist/hooks/registry.js +38 -1
- package/dist/hooks/shell-runner.d.ts +2 -0
- package/dist/hooks/shell-runner.js +43 -2
- package/dist/index.d.ts +31 -6
- package/dist/index.js +30 -3
- package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
- package/dist/llm/capabilities/reasoning-control.js +38 -0
- package/dist/llm/capabilities/rules.js +33 -3
- package/dist/llm/capabilities/types.d.ts +22 -2
- package/dist/llm/clamp-max-tokens.d.ts +11 -0
- package/dist/llm/clamp-max-tokens.js +17 -0
- package/dist/llm/client-base.d.ts +41 -4
- package/dist/llm/client-base.js +67 -13
- package/dist/llm/client-factory.d.ts +3 -3
- package/dist/llm/client-factory.js +2 -2
- package/dist/llm/model-pool.d.ts +15 -11
- package/dist/llm/model-pool.js +23 -20
- package/dist/llm/provider-catalog.d.ts +2 -2
- package/dist/llm/providers/anthropic.d.ts +32 -2
- package/dist/llm/providers/anthropic.js +147 -8
- package/dist/llm/providers/openai.d.ts +11 -2
- package/dist/llm/providers/openai.js +253 -52
- package/dist/llm/reasoning-setting.d.ts +51 -0
- package/dist/llm/reasoning-setting.js +32 -0
- package/dist/llm/stop-reason.d.ts +13 -0
- package/dist/llm/stop-reason.js +21 -0
- package/dist/llm/strip-vision.d.ts +32 -0
- package/dist/llm/strip-vision.js +94 -0
- package/dist/llm/types.d.ts +5 -6
- package/dist/logging/logger.js +18 -4
- package/dist/logging/sanitize-messages.d.ts +10 -0
- package/dist/logging/sanitize-messages.js +90 -0
- package/dist/lsp/client.js +28 -8
- package/dist/lsp/manager.js +2 -1
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/onboarding.js +18 -11
- package/dist/plugins/gitOps.d.ts +34 -0
- package/dist/plugins/gitOps.js +59 -3
- package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
- package/dist/plugins/installer/codex/convertAgents.js +55 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
- package/dist/plugins/installer/codex/convertMcp.js +45 -0
- package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
- package/dist/plugins/installer/codex/convertSkills.js +33 -0
- package/dist/plugins/installer/detectFormat.d.ts +2 -0
- package/dist/plugins/installer/detectFormat.js +6 -0
- package/dist/plugins/installer/install.d.ts +7 -0
- package/dist/plugins/installer/install.js +99 -0
- package/dist/plugins/installer/installFromSource.d.ts +13 -0
- package/dist/plugins/installer/installFromSource.js +45 -0
- package/dist/plugins/installer/list.d.ts +9 -0
- package/dist/plugins/installer/list.js +30 -0
- package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
- package/dist/plugins/installer/loadPluginAgents.js +23 -0
- package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
- package/dist/plugins/installer/loadPluginMcp.js +74 -0
- package/dist/plugins/installer/parseSource.d.ts +20 -0
- package/dist/plugins/installer/parseSource.js +74 -0
- package/dist/plugins/installer/paths.d.ts +5 -0
- package/dist/plugins/installer/paths.js +25 -0
- package/dist/plugins/installer/types.d.ts +49 -0
- package/dist/plugins/installer/types.js +27 -0
- package/dist/plugins/installer/uninstall.d.ts +2 -0
- package/dist/plugins/installer/uninstall.js +14 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +65 -0
- package/dist/plugins/loadPluginHooks.d.ts +8 -1
- package/dist/plugins/loadPluginHooks.js +11 -1
- package/dist/plugins/marketplaceManager.d.ts +8 -1
- package/dist/plugins/marketplaceManager.js +38 -4
- package/dist/plugins/pluginCommandHook.js +34 -0
- package/dist/plugins/pluginInstaller.d.ts +21 -0
- package/dist/plugins/pluginInstaller.js +128 -29
- package/dist/plugins/schemas.js +40 -6
- package/dist/plugins/types.d.ts +4 -0
- package/dist/preset/index.js +16 -0
- package/dist/prompt/composer.d.ts +4 -0
- package/dist/prompt/composer.js +28 -4
- package/dist/prompt/sections/orchestration.md +8 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -1
- package/dist/protocol/chat-session-manager.js +10 -0
- package/dist/protocol/chat-session.d.ts +17 -0
- package/dist/protocol/chat-session.js +28 -0
- package/dist/protocol/redact.d.ts +50 -0
- package/dist/protocol/redact.js +71 -0
- package/dist/protocol/server.d.ts +41 -2
- package/dist/protocol/server.js +231 -33
- package/dist/protocol/tcp-transport.d.ts +45 -0
- package/dist/protocol/tcp-transport.js +74 -0
- package/dist/protocol/types.d.ts +23 -0
- package/dist/protocol/types.js +6 -0
- package/dist/remote/bridge.d.ts +11 -2
- package/dist/remote/bridge.js +61 -33
- package/dist/run/ArtifactTracker.js +5 -4
- package/dist/run/EngineRunner.d.ts +27 -1
- package/dist/run/EngineRunner.js +46 -8
- package/dist/run/FileRunStore.js +32 -8
- package/dist/run/RunLock.d.ts +28 -2
- package/dist/run/RunLock.js +49 -7
- package/dist/run/RunManager.js +51 -3
- package/dist/run/RunQueue.d.ts +1 -0
- package/dist/run/RunQueue.js +8 -2
- package/dist/run/factory.d.ts +7 -0
- package/dist/run/factory.js +1 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +1 -1
- package/dist/run/redirect-target.d.ts +7 -0
- package/dist/run/redirect-target.js +13 -0
- package/dist/run/types.js +1 -1
- package/dist/services/browser-open.d.ts +13 -0
- package/dist/services/browser-open.js +16 -0
- package/dist/services/dream-consolidation.d.ts +57 -0
- package/dist/services/dream-consolidation.js +151 -0
- package/dist/services/memory-orchestrator.js +18 -0
- package/dist/services/notifier.d.ts +8 -0
- package/dist/services/notifier.js +39 -25
- package/dist/services/oauth.js +5 -7
- package/dist/services/session-memory-sort.d.ts +8 -0
- package/dist/services/session-memory-sort.js +9 -0
- package/dist/services/session-memory.js +8 -9
- package/dist/session/file-history.js +10 -3
- package/dist/session/session-manager.d.ts +23 -1
- package/dist/session/session-manager.js +73 -2
- package/dist/settings/disk-defaults.d.ts +35 -0
- package/dist/settings/disk-defaults.js +24 -0
- package/dist/settings/manager.d.ts +33 -0
- package/dist/settings/manager.js +94 -1
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema.d.ts +1142 -126
- package/dist/settings/schema.js +119 -10
- package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
- package/dist/tool-system/builtin/add-marketplace.js +76 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -4
- package/dist/tool-system/builtin/agent.d.ts +29 -10
- package/dist/tool-system/builtin/agent.js +108 -52
- package/dist/tool-system/builtin/apply-patch/index.js +22 -3
- package/dist/tool-system/builtin/arena.js +6 -7
- package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
- package/dist/tool-system/builtin/complete-goal.js +45 -0
- package/dist/tool-system/builtin/cron.js +53 -9
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +6 -1
- package/dist/tool-system/builtin/generate-image.d.ts +25 -0
- package/dist/tool-system/builtin/generate-image.js +146 -0
- package/dist/tool-system/builtin/glob.js +7 -0
- package/dist/tool-system/builtin/grep.js +6 -0
- package/dist/tool-system/builtin/index.d.ts +18 -1
- package/dist/tool-system/builtin/index.js +62 -1
- package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
- package/dist/tool-system/builtin/notebook-edit.js +8 -1
- package/dist/tool-system/builtin/read.d.ts +2 -1
- package/dist/tool-system/builtin/read.js +9 -1
- package/dist/tool-system/builtin/sleep.js +9 -3
- package/dist/tool-system/builtin/task.js +4 -1
- package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
- package/dist/tool-system/builtin/update-automation-memory.js +60 -0
- package/dist/tool-system/builtin/view-image.d.ts +18 -0
- package/dist/tool-system/builtin/view-image.js +96 -0
- package/dist/tool-system/builtin/web-search.d.ts +4 -1
- package/dist/tool-system/builtin/web-search.js +36 -3
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +7 -1
- package/dist/tool-system/context.d.ts +12 -0
- package/dist/tool-system/executor.d.ts +0 -1
- package/dist/tool-system/executor.js +34 -40
- package/dist/tool-system/investigation-guard.d.ts +4 -1
- package/dist/tool-system/investigation-guard.js +19 -3
- package/dist/tool-system/mcp-manager.d.ts +36 -12
- package/dist/tool-system/mcp-manager.js +173 -10
- package/dist/tool-system/path-policy.d.ts +94 -0
- package/dist/tool-system/path-policy.js +279 -0
- package/dist/tool-system/permission.js +43 -50
- package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
- package/dist/tool-system/plan-mode-allowlist.js +43 -0
- package/dist/tool-system/registry.js +12 -1
- package/dist/tool-system/validation.d.ts +10 -0
- package/dist/tool-system/validation.js +16 -3
- package/dist/types.d.ts +90 -15
- package/dist/utils/format.js +4 -2
- package/dist/utils/lockfile.js +9 -2
- package/dist/utils/theme.d.ts +6 -0
- package/dist/utils/theme.js +4 -2
- package/package.json +2 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
2
|
+
/**
|
|
3
|
+
* Redaction helpers for protocol query responses.
|
|
4
|
+
*
|
|
5
|
+
* The protocol exposes config snapshots to any connected client. Raw API keys,
|
|
6
|
+
* provider credentials, and authorization headers must never leave this
|
|
7
|
+
* boundary verbatim — the client can use derived fields (`hasApiKey`,
|
|
8
|
+
* `apiKeyPreview`) to render provider/model status without holding the secret.
|
|
9
|
+
*
|
|
10
|
+
* Two entry points:
|
|
11
|
+
* - redactConfigForResponse: rewrites the LLM section of the config-query
|
|
12
|
+
* payload, removing `apiKey` and adding derived presence/preview fields.
|
|
13
|
+
* - maskSecretValue: scrubs a single (key, value) pair returned by
|
|
14
|
+
* config_get, where the client picks the key — including `llm.apiKey`.
|
|
15
|
+
*/
|
|
16
|
+
/** Match dotted/camel/snake variants of obviously secret keys. */
|
|
17
|
+
const SECRET_KEY_RE = /(^|[._-])(api[_-]?key|authorization|x[_-]api[_-]key|bearer[_-]?token|access[_-]?token|refresh[_-]?token|secret|password|client[_-]?secret)($|[._-])/i;
|
|
18
|
+
/** Produce a short preview (`sk-…abcd`) so UIs can render presence without revealing the secret. */
|
|
19
|
+
export function makeApiKeyPreview(apiKey) {
|
|
20
|
+
if (typeof apiKey !== "string" || apiKey.length === 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (apiKey.length <= 8)
|
|
23
|
+
return "…";
|
|
24
|
+
return `${apiKey.slice(0, 3)}…${apiKey.slice(-4)}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Return true when a dotted setting key path points at a secret field.
|
|
28
|
+
* Used to decide whether `config_get`'s response should redact the value.
|
|
29
|
+
*/
|
|
30
|
+
export function isSecretKeyPath(key) {
|
|
31
|
+
return SECRET_KEY_RE.test(key);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Redact a single value returned by config_get.
|
|
35
|
+
*
|
|
36
|
+
* Two-stage rule:
|
|
37
|
+
* 1. If the caller-supplied key path itself looks secret
|
|
38
|
+
* ("llm.apiKey", "headers.authorization", …) → collapse to "[redacted]".
|
|
39
|
+
* 2. Otherwise, if the value is an object/array (e.g. config_get("llm") or
|
|
40
|
+
* config_get("providers")), walk it with the shared secret redactor
|
|
41
|
+
* from logging/sanitize-messages so nested apiKey / token / Bearer
|
|
42
|
+
* fields don't slip through verbatim. Primitives pass unchanged.
|
|
43
|
+
*
|
|
44
|
+
* Pre-fix this only inspected the top-level key string, so
|
|
45
|
+
* config_get("providers") returned an array of provider objects with raw
|
|
46
|
+
* apiKey values intact. That defeated the purpose of the boundary.
|
|
47
|
+
*/
|
|
48
|
+
export function maskSecretValue(key, value) {
|
|
49
|
+
if (isSecretKeyPath(key))
|
|
50
|
+
return "[redacted]";
|
|
51
|
+
if (value && typeof value === "object") {
|
|
52
|
+
return redactSecrets(value);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Rewrite the LLM section of a config snapshot for transport. We only forward
|
|
58
|
+
* fields the client legitimately needs to render status; secrets become
|
|
59
|
+
* derived flags.
|
|
60
|
+
*/
|
|
61
|
+
export function redactLlmConfig(llm) {
|
|
62
|
+
const preview = makeApiKeyPreview(llm.apiKey);
|
|
63
|
+
return {
|
|
64
|
+
provider: llm.provider,
|
|
65
|
+
model: llm.model,
|
|
66
|
+
baseUrl: llm.baseUrl,
|
|
67
|
+
maxTokens: llm.maxTokens,
|
|
68
|
+
hasApiKey: typeof llm.apiKey === "string" && llm.apiKey.length > 0,
|
|
69
|
+
...(preview ? { apiKeyPreview: preview } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { Transport } from "./transport.js";
|
|
17
17
|
import type { Engine } from "../engine/engine.js";
|
|
18
|
+
import type { ValidatedSettings } from "../settings/schema.js";
|
|
18
19
|
import type { ChatSessionManager } from "./chat-session-manager.js";
|
|
19
20
|
export interface AgentServerOptions {
|
|
20
21
|
/**
|
|
@@ -29,11 +30,38 @@ export interface AgentServerOptions {
|
|
|
29
30
|
*/
|
|
30
31
|
engine?: Engine;
|
|
31
32
|
transport: Transport;
|
|
33
|
+
/**
|
|
34
|
+
* Reads fresh settings from disk for config hot-reload (layer 2). MUST be
|
|
35
|
+
* the SAME closure the engineFactory uses for new sessions (e.g.
|
|
36
|
+
* agent-server-stdio's `freshSettings`) so a reloaded running session and a
|
|
37
|
+
* newly-created session converge on identical disk config — no divergence.
|
|
38
|
+
* When absent, `configure({ reloadSettings })` returns an explicit error.
|
|
39
|
+
*/
|
|
40
|
+
settingsReader?: () => ValidatedSettings;
|
|
32
41
|
}
|
|
33
42
|
export declare class AgentServer {
|
|
34
43
|
private readonly chatManager;
|
|
35
44
|
private readonly legacyEngine;
|
|
45
|
+
private globalQueryEngine;
|
|
36
46
|
private transport;
|
|
47
|
+
/** Fresh-disk settings reader for config hot-reload; null when not wired. */
|
|
48
|
+
private readonly settingsReader;
|
|
49
|
+
/**
|
|
50
|
+
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
51
|
+
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
52
|
+
*/
|
|
53
|
+
private configVersion;
|
|
54
|
+
/**
|
|
55
|
+
* JSON of the last disk-default patch broadcast to ALL live sessions (#6).
|
|
56
|
+
* When a new reloadSettings request produces a byte-identical patch we SKIP
|
|
57
|
+
* the entire forEachSession broadcast — every session's refreshRuntimeConfig
|
|
58
|
+
* would otherwise unconditionally reloadHooks() (invalidate + full disk
|
|
59
|
+
* re-read + hook teardown/re-register) per call, and the personalization UI
|
|
60
|
+
* auto-saves on a 600ms debounce, so identical re-saves caused K× hook churn
|
|
61
|
+
* per keystroke-pause. A genuine change still differs in JSON → propagates.
|
|
62
|
+
* null until the first broadcast.
|
|
63
|
+
*/
|
|
64
|
+
private lastBroadcastPatch;
|
|
37
65
|
private running;
|
|
38
66
|
private abortController;
|
|
39
67
|
/** Pending approval requests: requestId → resolve function (legacy path) */
|
|
@@ -67,11 +95,22 @@ export declare class AgentServer {
|
|
|
67
95
|
/**
|
|
68
96
|
* Ask the client to answer a question from the agent (legacy single-engine path).
|
|
69
97
|
*/
|
|
98
|
+
/**
|
|
99
|
+
* Per-session AskUserQuestion for the chatManager path. Mirrors
|
|
100
|
+
* requestAskUserFromClient but resolves via the SESSION's pendingApprovals
|
|
101
|
+
* (the chatManager approve handler looks there, keyed by sessionId+requestId)
|
|
102
|
+
* and tags the notify with sessionId so the renderer routes the question to
|
|
103
|
+
* the right chat tab.
|
|
104
|
+
*/
|
|
105
|
+
private requestAskUserForSession;
|
|
70
106
|
private requestAskUserFromClient;
|
|
71
107
|
private notify;
|
|
72
108
|
/**
|
|
73
|
-
* Get any available engine — used for global query ops when
|
|
74
|
-
* present but no legacyEngine was supplied.
|
|
109
|
+
* Get any available engine — used for global query/configure ops when
|
|
110
|
+
* chatManager is present but no legacyEngine was supplied. Prefer a live chat
|
|
111
|
+
* session; before the first user turn, lazily build a detached engine through
|
|
112
|
+
* the manager's factory so global UI surfaces (/login, model manager, model
|
|
113
|
+
* selector) can query models/providers without requiring a chat session first.
|
|
75
114
|
*/
|
|
76
115
|
private anyEngine;
|
|
77
116
|
close(): void;
|
package/dist/protocol/server.js
CHANGED
|
@@ -14,14 +14,36 @@
|
|
|
14
14
|
* falls back to borrowing any live session's engine for those reads.
|
|
15
15
|
*/
|
|
16
16
|
import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotification, isRequest, } from "./types.js";
|
|
17
|
+
import { diskDefaultsFrom } from "../engine/engine.js";
|
|
17
18
|
import { setInteractiveApprovalFn } from "../tool-system/permission.js";
|
|
18
19
|
import { getArenaStatus } from "../tool-system/builtin/arena.js";
|
|
19
20
|
import { agentNotificationBus } from "../tool-system/builtin/agent-notifications.js";
|
|
20
21
|
import { nanoid } from "nanoid";
|
|
22
|
+
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
23
|
+
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
21
24
|
export class AgentServer {
|
|
22
25
|
chatManager;
|
|
23
26
|
legacyEngine;
|
|
27
|
+
globalQueryEngine = null;
|
|
24
28
|
transport;
|
|
29
|
+
/** Fresh-disk settings reader for config hot-reload; null when not wired. */
|
|
30
|
+
settingsReader;
|
|
31
|
+
/**
|
|
32
|
+
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
33
|
+
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
34
|
+
*/
|
|
35
|
+
configVersion = 0;
|
|
36
|
+
/**
|
|
37
|
+
* JSON of the last disk-default patch broadcast to ALL live sessions (#6).
|
|
38
|
+
* When a new reloadSettings request produces a byte-identical patch we SKIP
|
|
39
|
+
* the entire forEachSession broadcast — every session's refreshRuntimeConfig
|
|
40
|
+
* would otherwise unconditionally reloadHooks() (invalidate + full disk
|
|
41
|
+
* re-read + hook teardown/re-register) per call, and the personalization UI
|
|
42
|
+
* auto-saves on a 600ms debounce, so identical re-saves caused K× hook churn
|
|
43
|
+
* per keystroke-pause. A genuine change still differs in JSON → propagates.
|
|
44
|
+
* null until the first broadcast.
|
|
45
|
+
*/
|
|
46
|
+
lastBroadcastPatch = null;
|
|
25
47
|
// ── Legacy single-engine state (used when chatManager is null) ──
|
|
26
48
|
running = false;
|
|
27
49
|
abortController = null;
|
|
@@ -41,6 +63,7 @@ export class AgentServer {
|
|
|
41
63
|
constructor(options) {
|
|
42
64
|
this.chatManager = options.chatManager ?? null;
|
|
43
65
|
this.legacyEngine = options.engine ?? null;
|
|
66
|
+
this.settingsReader = options.settingsReader ?? null;
|
|
44
67
|
if (!this.chatManager && !this.legacyEngine) {
|
|
45
68
|
throw new Error("AgentServer: either chatManager or engine must be supplied");
|
|
46
69
|
}
|
|
@@ -53,16 +76,19 @@ export class AgentServer {
|
|
|
53
76
|
});
|
|
54
77
|
}
|
|
55
78
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
setInteractiveApprovalFn((request) => {
|
|
80
|
+
return this.requestApprovalFromClient(request);
|
|
81
|
+
});
|
|
59
82
|
if (this.legacyEngine) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
// Only wire an interactive askUser when a human is present. For
|
|
84
|
+
// unattended (headless) runs we leave askUser undefined so
|
|
85
|
+
// AskUserQuestion hits its headless-error branch and returns immediately
|
|
86
|
+
// instead of suspending until the tool-exec timeout (~300s).
|
|
87
|
+
if (!this.legacyEngine.isHeadless()) {
|
|
88
|
+
this.legacyEngine.setAskUser((question, opts) => {
|
|
89
|
+
return this.requestAskUserFromClient(question, opts);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
66
92
|
}
|
|
67
93
|
// B2.2 — forward background sub-agent completion events through the
|
|
68
94
|
// protocol so Desktop / SDK / remote AgentClients see them too. The
|
|
@@ -140,9 +166,24 @@ export class AgentServer {
|
|
|
140
166
|
session.engine.setPlanMode(params.planMode);
|
|
141
167
|
}
|
|
142
168
|
const sid = params.sessionId;
|
|
169
|
+
// Wire AskUserQuestion for this interactive session. The chatManager path
|
|
170
|
+
// builds a fresh per-session Engine via engineFactory, which (unlike the
|
|
171
|
+
// legacy single-engine path) never had askUser wired — so AskUserQuestion
|
|
172
|
+
// always fell into its "not available in headless mode" branch in normal
|
|
173
|
+
// chat (and in a resumed automation session). Route it to the client with
|
|
174
|
+
// this session's id so the renderer attributes the question to the right
|
|
175
|
+
// tab, resolving via the session's own pendingApprovals. Skip genuinely
|
|
176
|
+
// headless engines (no human to answer).
|
|
177
|
+
if (!session.engine.isHeadless()) {
|
|
178
|
+
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
179
|
+
}
|
|
143
180
|
try {
|
|
144
181
|
const result = await session.enqueueTurn(params.task, {
|
|
145
182
|
cwd: params.cwd,
|
|
183
|
+
goal: typeof params.goal === "string" ||
|
|
184
|
+
(params.goal != null && typeof params.goal === "object")
|
|
185
|
+
? params.goal
|
|
186
|
+
: undefined,
|
|
146
187
|
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId: sid, event }),
|
|
147
188
|
});
|
|
148
189
|
const runResult = {
|
|
@@ -191,12 +232,20 @@ export class AgentServer {
|
|
|
191
232
|
};
|
|
192
233
|
// TodoWrite emits task_update directly through ToolContext.streamCallback;
|
|
193
234
|
// no module singleton wiring needed anymore.
|
|
235
|
+
// Snapshot the controller for the catch block — `this.abortController`
|
|
236
|
+
// is nulled in the `finally`, so by the time we'd want to check
|
|
237
|
+
// `signal.aborted` below it's already gone.
|
|
238
|
+
const runController = this.abortController;
|
|
194
239
|
try {
|
|
195
240
|
const result = await this.legacyEngine.run(params.task, {
|
|
196
241
|
cwd: params.cwd,
|
|
197
242
|
sessionId: params.sessionId,
|
|
198
|
-
signal:
|
|
243
|
+
signal: runController.signal,
|
|
199
244
|
onStream: streamToClient,
|
|
245
|
+
goal: typeof params.goal === "string" ||
|
|
246
|
+
(params.goal != null && typeof params.goal === "object")
|
|
247
|
+
? params.goal
|
|
248
|
+
: undefined,
|
|
200
249
|
});
|
|
201
250
|
const runResult = {
|
|
202
251
|
text: result.text,
|
|
@@ -208,7 +257,28 @@ export class AgentServer {
|
|
|
208
257
|
this.transport.send(createResponse(req.id, runResult));
|
|
209
258
|
}
|
|
210
259
|
catch (err) {
|
|
211
|
-
|
|
260
|
+
// ESC / Stop path. If the user cancelled, the engine's
|
|
261
|
+
// in-flight LLM call or Bash spawn rejects with an AbortError
|
|
262
|
+
// that bubbles up here. Don't surface it as InternalError —
|
|
263
|
+
// the user already knows they pressed stop; clients should
|
|
264
|
+
// just clear busy.
|
|
265
|
+
const errName = err.name;
|
|
266
|
+
const aborted = runController.signal.aborted ||
|
|
267
|
+
errName === "AbortError" ||
|
|
268
|
+
errName === "APIUserAbortError";
|
|
269
|
+
if (aborted) {
|
|
270
|
+
const cancelledResult = {
|
|
271
|
+
text: "",
|
|
272
|
+
reason: "aborted_streaming",
|
|
273
|
+
sessionId: params.sessionId ?? "",
|
|
274
|
+
turnCount: 0,
|
|
275
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
276
|
+
};
|
|
277
|
+
this.transport.send(createResponse(req.id, cancelledResult));
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
281
|
+
}
|
|
212
282
|
}
|
|
213
283
|
finally {
|
|
214
284
|
this.running = false;
|
|
@@ -306,6 +376,29 @@ export class AgentServer {
|
|
|
306
376
|
if (typeof params.permissionMode === "string") {
|
|
307
377
|
s.engine.setPermissionMode(params.permissionMode);
|
|
308
378
|
}
|
|
379
|
+
// Per-session model switch — the missing piece that made model changes
|
|
380
|
+
// worker-global (session-isolation research §3). requestModelSwitch
|
|
381
|
+
// applies immediately when idle, defers to the run boundary when busy
|
|
382
|
+
// so it never swaps the model under a running LLM client.
|
|
383
|
+
if (typeof params.model === "string") {
|
|
384
|
+
try {
|
|
385
|
+
s.requestModelSwitch(params.model);
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Hot-reload disk-default config onto this one session (layer 2).
|
|
393
|
+
if (params.reloadSettings === true) {
|
|
394
|
+
if (!this.settingsReader) {
|
|
395
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "reloadSettings is not supported by this AgentServer: no settingsReader is wired"));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const settings = this.settingsReader();
|
|
399
|
+
const version = ++this.configVersion;
|
|
400
|
+
s.engine.refreshRuntimeConfig(diskDefaultsFrom(settings), version);
|
|
401
|
+
}
|
|
309
402
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
310
403
|
return;
|
|
311
404
|
}
|
|
@@ -335,6 +428,42 @@ export class AgentServer {
|
|
|
335
428
|
if (params.planMode !== undefined && engine) {
|
|
336
429
|
engine.setPlanMode(params.planMode);
|
|
337
430
|
}
|
|
431
|
+
// Config hot-reload layer 2: re-read disk settings once and push the
|
|
432
|
+
// disk-default config patch (+ settings-hook reload + incremental MCP
|
|
433
|
+
// connect) onto every live session. Parallel to reloadModels; no new
|
|
434
|
+
// protocol method. In-flight turns are untouched — refreshRuntimeConfig
|
|
435
|
+
// only mutates this.config, picked up at the next turn boundary.
|
|
436
|
+
if (params.reloadSettings === true) {
|
|
437
|
+
if (!this.settingsReader) {
|
|
438
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "reloadSettings is not supported by this AgentServer: no settingsReader is wired"));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!this.chatManager) {
|
|
442
|
+
// Single-engine host (legacyEngine / anyEngine, no chatManager): there
|
|
443
|
+
// are no sessions to fan out to, but the one engine must still pick up
|
|
444
|
+
// the reload — otherwise we'd report ok:true while silently dropping it.
|
|
445
|
+
// Mirrors how reloadModels/model/planMode above act on `engine`.
|
|
446
|
+
if (engine) {
|
|
447
|
+
const settings = this.settingsReader();
|
|
448
|
+
engine.refreshRuntimeConfig(diskDefaultsFrom(settings), ++this.configVersion);
|
|
449
|
+
}
|
|
450
|
+
this.transport.send(createResponse(req.id, { ok: true }));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const settings = this.settingsReader();
|
|
454
|
+
const patch = diskDefaultsFrom(settings);
|
|
455
|
+
// #6: content short-circuit. If the disk-default patch is byte-identical
|
|
456
|
+
// to the last one we broadcast, the disk config that matters to running
|
|
457
|
+
// sessions hasn't changed — skip the whole forEachSession (and the per-
|
|
458
|
+
// session reloadHooks churn it triggers). Still return ok; a genuine
|
|
459
|
+
// change differs in JSON and propagates normally.
|
|
460
|
+
const patchJson = JSON.stringify(patch);
|
|
461
|
+
if (patchJson !== this.lastBroadcastPatch) {
|
|
462
|
+
this.lastBroadcastPatch = patchJson;
|
|
463
|
+
const version = ++this.configVersion;
|
|
464
|
+
this.chatManager.forEachSession((s) => s.engine.refreshRuntimeConfig(patch, version));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
338
467
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
339
468
|
}
|
|
340
469
|
// ─── Query ──────────────────────────────────────────────────────
|
|
@@ -361,12 +490,15 @@ export class AgentServer {
|
|
|
361
490
|
case "sessions": {
|
|
362
491
|
if (this.chatManager) {
|
|
363
492
|
// Return the ChatSessionManager's live sessions
|
|
364
|
-
const sessions = [
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
493
|
+
const sessions = [];
|
|
494
|
+
this.chatManager.forEachSession((s) => {
|
|
495
|
+
sessions.push({
|
|
496
|
+
sessionId: s.id,
|
|
497
|
+
busy: s.isBusy(),
|
|
498
|
+
queueDepth: s.queueDepth(),
|
|
499
|
+
lastActivityAt: s.lastActivityAt,
|
|
500
|
+
});
|
|
501
|
+
});
|
|
370
502
|
this.transport.send(createResponse(req.id, { type: "sessions", data: sessions }));
|
|
371
503
|
}
|
|
372
504
|
else if (engine) {
|
|
@@ -393,15 +525,9 @@ export class AgentServer {
|
|
|
393
525
|
model: config.llm.model,
|
|
394
526
|
cwd: config.cwd,
|
|
395
527
|
maxContextTokens: config.maxContextTokens,
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
apiKey: config.llm.apiKey,
|
|
400
|
-
baseUrl: config.llm.baseUrl,
|
|
401
|
-
temperature: config.llm.temperature,
|
|
402
|
-
maxTokens: config.llm.maxTokens,
|
|
403
|
-
enableStreaming: config.llm.enableStreaming,
|
|
404
|
-
},
|
|
528
|
+
// apiKey is redacted at this boundary: clients only get
|
|
529
|
+
// hasApiKey + optional apiKeyPreview. See protocol/redact.ts.
|
|
530
|
+
llm: redactLlmConfig(config.llm),
|
|
405
531
|
},
|
|
406
532
|
}));
|
|
407
533
|
break;
|
|
@@ -499,7 +625,15 @@ export class AgentServer {
|
|
|
499
625
|
cachedAt: cache ? cache.fetchedAt : undefined,
|
|
500
626
|
};
|
|
501
627
|
});
|
|
502
|
-
|
|
628
|
+
// Strip apiKey / other secret-shaped fields from each provider
|
|
629
|
+
// before sending. `...p` above spreads the whole provider record,
|
|
630
|
+
// which includes apiKey verbatim — without this redact pass, any
|
|
631
|
+
// protocol client could call query("providers") and harvest
|
|
632
|
+
// credentials. The shared redactor preserves
|
|
633
|
+
// presence-without-value (hasApiKey-style consumers still see the
|
|
634
|
+
// field shape, just with [redacted] in place of the secret).
|
|
635
|
+
const safe = redactSecrets(enriched);
|
|
636
|
+
this.transport.send(createResponse(req.id, { type: "providers", data: safe }));
|
|
503
637
|
}
|
|
504
638
|
catch (err) {
|
|
505
639
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -521,7 +655,11 @@ export class AgentServer {
|
|
|
521
655
|
if (!key)
|
|
522
656
|
throw new Error("key is required for config_set");
|
|
523
657
|
engine.updateConfig(key, value);
|
|
524
|
-
|
|
658
|
+
// Echo back through the same secret-aware masker as config_get so
|
|
659
|
+
// a `config_set("llm.apiKey", "...")` confirmation doesn't ship
|
|
660
|
+
// the new secret through the response/log path.
|
|
661
|
+
const safeValue = maskSecretValue(key, value);
|
|
662
|
+
this.transport.send(createResponse(req.id, { type: "config_set", data: { key, value: safeValue } }));
|
|
525
663
|
}
|
|
526
664
|
catch (err) {
|
|
527
665
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -538,7 +676,9 @@ export class AgentServer {
|
|
|
538
676
|
if (!key)
|
|
539
677
|
throw new Error("key is required for config_get");
|
|
540
678
|
const value = engine.readSetting(key);
|
|
541
|
-
|
|
679
|
+
// Mask secret-looking keys (apiKey/token/secret/…) at the boundary.
|
|
680
|
+
const safeValue = maskSecretValue(key, value);
|
|
681
|
+
this.transport.send(createResponse(req.id, { type: "config_get", data: { key, value: safeValue } }));
|
|
542
682
|
}
|
|
543
683
|
catch (err) {
|
|
544
684
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -743,6 +883,54 @@ export class AgentServer {
|
|
|
743
883
|
/**
|
|
744
884
|
* Ask the client to answer a question from the agent (legacy single-engine path).
|
|
745
885
|
*/
|
|
886
|
+
/**
|
|
887
|
+
* Per-session AskUserQuestion for the chatManager path. Mirrors
|
|
888
|
+
* requestAskUserFromClient but resolves via the SESSION's pendingApprovals
|
|
889
|
+
* (the chatManager approve handler looks there, keyed by sessionId+requestId)
|
|
890
|
+
* and tags the notify with sessionId so the renderer routes the question to
|
|
891
|
+
* the right chat tab.
|
|
892
|
+
*/
|
|
893
|
+
requestAskUserForSession(session, sessionId, question, opts) {
|
|
894
|
+
return new Promise((resolve) => {
|
|
895
|
+
const requestId = nanoid(12);
|
|
896
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
897
|
+
this.clearApprovalTimer(requestId);
|
|
898
|
+
const result = decision;
|
|
899
|
+
if (result && typeof result === "object" && "approved" in result) {
|
|
900
|
+
resolve(result.approved ? (result.answer ?? "") : (result.reason ?? "(user declined to answer)"));
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
// chatManager approve handler resolves with the raw decision value.
|
|
904
|
+
resolve(typeof decision === "string" ? decision : "");
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
const timer = setTimeout(() => {
|
|
908
|
+
if (session.pendingApprovals.has(requestId)) {
|
|
909
|
+
session.pendingApprovals.delete(requestId);
|
|
910
|
+
this.approvalTimers.delete(requestId);
|
|
911
|
+
resolve("(approval timed out)");
|
|
912
|
+
}
|
|
913
|
+
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
914
|
+
this.approvalTimers.set(requestId, timer);
|
|
915
|
+
const args = { question };
|
|
916
|
+
if (opts?.header !== undefined)
|
|
917
|
+
args.header = opts.header;
|
|
918
|
+
if (opts?.options !== undefined)
|
|
919
|
+
args.options = opts.options;
|
|
920
|
+
if (opts?.multiSelect !== undefined)
|
|
921
|
+
args.multiSelect = opts.multiSelect;
|
|
922
|
+
this.notify(Methods.ApprovalRequest, {
|
|
923
|
+
sessionId,
|
|
924
|
+
requestId,
|
|
925
|
+
request: {
|
|
926
|
+
toolName: "__ask_user__",
|
|
927
|
+
args,
|
|
928
|
+
description: question,
|
|
929
|
+
riskLevel: "low",
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
}
|
|
746
934
|
requestAskUserFromClient(question, opts) {
|
|
747
935
|
return new Promise((resolve) => {
|
|
748
936
|
const requestId = nanoid(12);
|
|
@@ -785,16 +973,26 @@ export class AgentServer {
|
|
|
785
973
|
this.transport.send(createNotification(method, params));
|
|
786
974
|
}
|
|
787
975
|
/**
|
|
788
|
-
* Get any available engine — used for global query ops when
|
|
789
|
-
* present but no legacyEngine was supplied.
|
|
976
|
+
* Get any available engine — used for global query/configure ops when
|
|
977
|
+
* chatManager is present but no legacyEngine was supplied. Prefer a live chat
|
|
978
|
+
* session; before the first user turn, lazily build a detached engine through
|
|
979
|
+
* the manager's factory so global UI surfaces (/login, model manager, model
|
|
980
|
+
* selector) can query models/providers without requiring a chat session first.
|
|
790
981
|
*/
|
|
791
982
|
anyEngine() {
|
|
792
983
|
if (!this.chatManager)
|
|
793
984
|
return null;
|
|
794
|
-
// Access private `sessions` map — acceptable for same-package access.
|
|
795
985
|
const sessions = this.chatManager.sessions;
|
|
796
986
|
const first = sessions.values().next().value;
|
|
797
|
-
|
|
987
|
+
if (first)
|
|
988
|
+
return first.engine;
|
|
989
|
+
if (this.globalQueryEngine)
|
|
990
|
+
return this.globalQueryEngine;
|
|
991
|
+
const factory = this.chatManager.factory;
|
|
992
|
+
if (typeof factory !== "function")
|
|
993
|
+
return null;
|
|
994
|
+
this.globalQueryEngine = factory({});
|
|
995
|
+
return this.globalQueryEngine;
|
|
798
996
|
}
|
|
799
997
|
// ─── Lifecycle ──────────────────────────────────────────────────
|
|
800
998
|
close() {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TcpTransport — newline-delimited JSON over a TCP socket (Duplex stream).
|
|
3
|
+
*
|
|
4
|
+
* Phase 6 of the automation plan: lets the same AgentServer that runs over
|
|
5
|
+
* stdio (agent-server-stdio) also serve over a network socket, so a headless
|
|
6
|
+
* host can be reached by a remote/local client. The transport implements the
|
|
7
|
+
* 3-method Transport interface (send/onMessage/close); AgentServer is unchanged
|
|
8
|
+
* (§0 of docs/automation-plan-2026-05-31.md).
|
|
9
|
+
*
|
|
10
|
+
* Framing mirrors StdioTransport: one JSON value per line. The socket core is
|
|
11
|
+
* separated from listen/accept (`listenTcp`) so the framing is unit-testable
|
|
12
|
+
* against any Duplex without opening a real port.
|
|
13
|
+
*
|
|
14
|
+
* v1 security: intended for localhost / SSH-tunnel use only. There is NO
|
|
15
|
+
* authentication here — do not bind to a public interface without adding auth.
|
|
16
|
+
*/
|
|
17
|
+
import { type Server, type Socket } from "node:net";
|
|
18
|
+
import type { Duplex } from "node:stream";
|
|
19
|
+
import type { Transport } from "./transport.js";
|
|
20
|
+
import type { RpcMessage } from "./types.js";
|
|
21
|
+
/** Wrap a connected Duplex socket as a Transport (NDJSON framing). */
|
|
22
|
+
export declare class SocketTransport implements Transport {
|
|
23
|
+
private readonly socket;
|
|
24
|
+
private rl;
|
|
25
|
+
private handlers;
|
|
26
|
+
constructor(socket: Duplex);
|
|
27
|
+
send(message: RpcMessage): void;
|
|
28
|
+
onMessage(handler: (message: RpcMessage) => void): void;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
export interface TcpListenResult {
|
|
32
|
+
server: Server;
|
|
33
|
+
/** The actual port bound (useful when 0 was requested). */
|
|
34
|
+
port: number;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Listen on host:port and invoke `onConnection` with a SocketTransport for each
|
|
39
|
+
* accepted client. Resolves once listening. Defaults to 127.0.0.1 — never bind
|
|
40
|
+
* 0.0.0.0 without auth (v1 has none).
|
|
41
|
+
*/
|
|
42
|
+
export declare function listenTcp(opts: {
|
|
43
|
+
port: number;
|
|
44
|
+
host?: string;
|
|
45
|
+
}, onConnection: (transport: SocketTransport, socket: Socket) => void): Promise<TcpListenResult>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TcpTransport — newline-delimited JSON over a TCP socket (Duplex stream).
|
|
3
|
+
*
|
|
4
|
+
* Phase 6 of the automation plan: lets the same AgentServer that runs over
|
|
5
|
+
* stdio (agent-server-stdio) also serve over a network socket, so a headless
|
|
6
|
+
* host can be reached by a remote/local client. The transport implements the
|
|
7
|
+
* 3-method Transport interface (send/onMessage/close); AgentServer is unchanged
|
|
8
|
+
* (§0 of docs/automation-plan-2026-05-31.md).
|
|
9
|
+
*
|
|
10
|
+
* Framing mirrors StdioTransport: one JSON value per line. The socket core is
|
|
11
|
+
* separated from listen/accept (`listenTcp`) so the framing is unit-testable
|
|
12
|
+
* against any Duplex without opening a real port.
|
|
13
|
+
*
|
|
14
|
+
* v1 security: intended for localhost / SSH-tunnel use only. There is NO
|
|
15
|
+
* authentication here — do not bind to a public interface without adding auth.
|
|
16
|
+
*/
|
|
17
|
+
import { createServer } from "node:net";
|
|
18
|
+
import { createInterface } from "node:readline";
|
|
19
|
+
/** Wrap a connected Duplex socket as a Transport (NDJSON framing). */
|
|
20
|
+
export class SocketTransport {
|
|
21
|
+
socket;
|
|
22
|
+
rl;
|
|
23
|
+
handlers = [];
|
|
24
|
+
constructor(socket) {
|
|
25
|
+
this.socket = socket;
|
|
26
|
+
this.rl = createInterface({ input: socket });
|
|
27
|
+
this.rl.on("line", (line) => {
|
|
28
|
+
if (!line.trim())
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
const msg = JSON.parse(line);
|
|
32
|
+
for (const handler of this.handlers)
|
|
33
|
+
handler(msg);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Skip malformed lines (same policy as StdioTransport).
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
send(message) {
|
|
41
|
+
this.socket.write(JSON.stringify(message) + "\n");
|
|
42
|
+
}
|
|
43
|
+
onMessage(handler) {
|
|
44
|
+
this.handlers.push(handler);
|
|
45
|
+
}
|
|
46
|
+
close() {
|
|
47
|
+
this.rl.close();
|
|
48
|
+
this.handlers = [];
|
|
49
|
+
this.socket.end();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Listen on host:port and invoke `onConnection` with a SocketTransport for each
|
|
54
|
+
* accepted client. Resolves once listening. Defaults to 127.0.0.1 — never bind
|
|
55
|
+
* 0.0.0.0 without auth (v1 has none).
|
|
56
|
+
*/
|
|
57
|
+
export function listenTcp(opts, onConnection) {
|
|
58
|
+
const host = opts.host ?? "127.0.0.1";
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const server = createServer((socket) => {
|
|
61
|
+
onConnection(new SocketTransport(socket), socket);
|
|
62
|
+
});
|
|
63
|
+
server.once("error", reject);
|
|
64
|
+
server.listen(opts.port, host, () => {
|
|
65
|
+
const addr = server.address();
|
|
66
|
+
const port = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
67
|
+
resolve({
|
|
68
|
+
server,
|
|
69
|
+
port,
|
|
70
|
+
close: () => new Promise((res) => server.close(() => res())),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|