@cjhyy/code-shell-core 0.5.0-rc.0 → 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 +29 -0
- package/dist/agent/agent-definition-registry.js +74 -0
- package/dist/agent/agent-definition.d.ts +37 -0
- package/dist/agent/agent-definition.js +59 -0
- 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 +118 -18
- 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 +256 -16
- package/dist/engine/engine.js +856 -200
- 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 +32 -7
- 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 +10 -0
- package/dist/prompt/composer.js +29 -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 +45 -1
- package/dist/settings/manager.js +121 -17
- package/dist/settings/manager.test.d.ts +1 -0
- package/dist/settings/manager.test.js +73 -0
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema.d.ts +1163 -126
- package/dist/settings/schema.js +126 -10
- package/dist/skills/scanner.d.ts +13 -6
- package/dist/skills/scanner.js +23 -4
- 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 +15 -0
- package/dist/tool-system/builtin/agent-registry.js +14 -4
- package/dist/tool-system/builtin/agent.d.ts +55 -0
- package/dist/tool-system/builtin/agent.js +165 -17
- 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/skill.js +23 -5
- 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 +43 -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
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* source types (path / git / github / git-subdir), no dependencies, no
|
|
5
5
|
* sparse-checkout, no npm/pip.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { existsSync, realpathSync, rmSync } from "node:fs";
|
|
8
|
+
import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
9
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
9
10
|
import { homedir, tmpdir } from "node:os";
|
|
10
|
-
import { gitClone, gitRevParseHead, githubRepoToCloneUrl } from "./gitOps.js";
|
|
11
|
+
import { gitClone, gitRevParseHead, gitSparseCheckoutAdd, githubRepoToCloneUrl, } from "./gitOps.js";
|
|
11
12
|
import { loadMarketplace } from "./marketplaceManager.js";
|
|
12
13
|
import { readKnownMarketplaces } from "./knownMarketplaces.js";
|
|
13
14
|
import { appendInstallEntry, pluginInstallKey, readInstalledPlugins, removeInstallEntries, } from "./installedPlugins.js";
|
|
@@ -21,6 +22,47 @@ function pluginCacheRoot() {
|
|
|
21
22
|
function pluginCacheDir(marketplace, plugin, version) {
|
|
22
23
|
return join(pluginCacheRoot(), marketplace, plugin, version);
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a candidate `installPath` to a realpath and verify it lives strictly
|
|
27
|
+
* underneath the plugin cache root. Returns the resolved path on success, or
|
|
28
|
+
* null when the path is missing, escapes the cache, equals the cache root
|
|
29
|
+
* itself, or is otherwise unsafe to remove.
|
|
30
|
+
*
|
|
31
|
+
* Exported so tests can exercise the safety predicate directly without
|
|
32
|
+
* arranging a full uninstall — the contract this defends (no rmSync outside
|
|
33
|
+
* the plugin cache) is the entire point of T2 / Workstream A2.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSafePluginPath(installPath, cacheRoot) {
|
|
36
|
+
if (typeof installPath !== "string" || installPath.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
// Realpath the cache root once. We require it to exist; if the cache root
|
|
39
|
+
// itself can't be resolved, refuse all deletions rather than fall back to
|
|
40
|
+
// a string-only check.
|
|
41
|
+
let safeCacheRoot;
|
|
42
|
+
try {
|
|
43
|
+
safeCacheRoot = realpathSync(cacheRoot);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// Realpath the target. A missing target (dangling symlink, already removed,
|
|
49
|
+
// typo in installed_plugins.json) returns null — caller will skip silently.
|
|
50
|
+
let resolvedTarget;
|
|
51
|
+
try {
|
|
52
|
+
resolvedTarget = realpathSync(installPath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// Strict containment: must start with cacheRoot + separator. Equal-to-root
|
|
58
|
+
// is explicitly rejected so a tampered entry can't wipe the whole cache.
|
|
59
|
+
const rootWithSep = safeCacheRoot.endsWith(sep) ? safeCacheRoot : safeCacheRoot + sep;
|
|
60
|
+
if (resolvedTarget === safeCacheRoot)
|
|
61
|
+
return null;
|
|
62
|
+
if (!resolvedTarget.startsWith(rootWithSep))
|
|
63
|
+
return null;
|
|
64
|
+
return resolvedTarget;
|
|
65
|
+
}
|
|
24
66
|
async function materializePath(marketplaceInstallLocation, relativePath, cacheTarget) {
|
|
25
67
|
const src = isAbsolute(relativePath)
|
|
26
68
|
? relativePath
|
|
@@ -28,16 +70,16 @@ async function materializePath(marketplaceInstallLocation, relativePath, cacheTa
|
|
|
28
70
|
if (!existsSync(src)) {
|
|
29
71
|
return { ok: false, error: `plugin source path "${relativePath}" not found in marketplace` };
|
|
30
72
|
}
|
|
31
|
-
|
|
73
|
+
await mkdir(dirname(cacheTarget), { recursive: true });
|
|
32
74
|
if (existsSync(cacheTarget))
|
|
33
|
-
|
|
34
|
-
|
|
75
|
+
await rm(cacheTarget, { recursive: true, force: true });
|
|
76
|
+
await cp(src, cacheTarget, { recursive: true });
|
|
35
77
|
return { ok: true, sha: undefined };
|
|
36
78
|
}
|
|
37
79
|
async function materializeGit(url, ref, cacheTarget) {
|
|
38
|
-
|
|
80
|
+
await mkdir(dirname(cacheTarget), { recursive: true });
|
|
39
81
|
if (existsSync(cacheTarget))
|
|
40
|
-
|
|
82
|
+
await rm(cacheTarget, { recursive: true, force: true });
|
|
41
83
|
const clone = await gitClone(url, cacheTarget, ref ? { ref } : undefined);
|
|
42
84
|
if (!clone.ok)
|
|
43
85
|
return { ok: false, error: clone.error };
|
|
@@ -48,7 +90,7 @@ async function materializeGit(url, ref, cacheTarget) {
|
|
|
48
90
|
}
|
|
49
91
|
async function materializeGitSubdir(url, subPath, ref, cacheTarget) {
|
|
50
92
|
// Clone to a tempdir, then copy the subdir over.
|
|
51
|
-
const tmp =
|
|
93
|
+
const tmp = await mkdtemp(join(tmpdir(), "plugin-clone-"));
|
|
52
94
|
try {
|
|
53
95
|
const clone = await gitClone(url, tmp + "/repo", ref ? { ref } : undefined);
|
|
54
96
|
if (!clone.ok)
|
|
@@ -63,14 +105,14 @@ async function materializeGitSubdir(url, subPath, ref, cacheTarget) {
|
|
|
63
105
|
error: `git-subdir path "${subPath}" not found in cloned repository`,
|
|
64
106
|
};
|
|
65
107
|
}
|
|
66
|
-
|
|
108
|
+
await mkdir(dirname(cacheTarget), { recursive: true });
|
|
67
109
|
if (existsSync(cacheTarget))
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
await rm(cacheTarget, { recursive: true, force: true });
|
|
111
|
+
await cp(src, cacheTarget, { recursive: true });
|
|
70
112
|
return { ok: true, sha: head.stdout };
|
|
71
113
|
}
|
|
72
114
|
finally {
|
|
73
|
-
|
|
115
|
+
await rm(tmp, { recursive: true, force: true });
|
|
74
116
|
}
|
|
75
117
|
}
|
|
76
118
|
function shortSha(sha) {
|
|
@@ -78,20 +120,49 @@ function shortSha(sha) {
|
|
|
78
120
|
return "unknown";
|
|
79
121
|
return sha.slice(0, 12);
|
|
80
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Compare a marketplace-declared SHA against the cloned HEAD. The
|
|
125
|
+
* declaration is allowed to be a short prefix (>= 7 chars, standard git
|
|
126
|
+
* abbreviation length); for stricter integrity guarantees marketplaces
|
|
127
|
+
* should pin the full 40-char hash. Case-insensitive.
|
|
128
|
+
*
|
|
129
|
+
* Exported so a unit test can pin the policy without spinning up a real
|
|
130
|
+
* clone.
|
|
131
|
+
*/
|
|
132
|
+
export function shaMatches(declared, actual) {
|
|
133
|
+
if (typeof declared !== "string" || typeof actual !== "string")
|
|
134
|
+
return false;
|
|
135
|
+
const d = declared.trim().toLowerCase();
|
|
136
|
+
const a = actual.trim().toLowerCase();
|
|
137
|
+
if (d.length < 7)
|
|
138
|
+
return false; // refuse 6-or-less; too collision-prone
|
|
139
|
+
if (d.length > 40)
|
|
140
|
+
return false; // not a valid SHA
|
|
141
|
+
return a.startsWith(d);
|
|
142
|
+
}
|
|
81
143
|
async function materialize(source, marketplaceInstallLocation, marketplace, plugin) {
|
|
82
144
|
// First pass: install into a placeholder dir, then rename to the SHA dir
|
|
83
145
|
// once we know it. For path sources there's no SHA — use "local".
|
|
84
146
|
const placeholder = pluginCacheDir(marketplace, plugin, "_pending_");
|
|
85
147
|
if (typeof source === "string") {
|
|
148
|
+
// The marketplace was cloned sparse (only the manifest dirs are on disk),
|
|
149
|
+
// so a local-path plugin's tree may not be checked out yet. Expand the
|
|
150
|
+
// sparse-checkout to include it before reading. Best-effort: on a
|
|
151
|
+
// non-sparse (full) clone this errors harmlessly and the files are already
|
|
152
|
+
// present, so we ignore the result and let materializePath report a real
|
|
153
|
+
// missing-path error if the subdir truly isn't there.
|
|
154
|
+
if (!isAbsolute(source)) {
|
|
155
|
+
await gitSparseCheckoutAdd(marketplaceInstallLocation, source);
|
|
156
|
+
}
|
|
86
157
|
const r = await materializePath(marketplaceInstallLocation, source, placeholder);
|
|
87
158
|
if (!r.ok)
|
|
88
159
|
return r;
|
|
89
160
|
const finalDir = pluginCacheDir(marketplace, plugin, "local");
|
|
90
161
|
if (existsSync(finalDir))
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
162
|
+
await rm(finalDir, { recursive: true, force: true });
|
|
163
|
+
await mkdir(dirname(finalDir), { recursive: true });
|
|
164
|
+
await cp(placeholder, finalDir, { recursive: true });
|
|
165
|
+
await rm(placeholder, { recursive: true, force: true });
|
|
95
166
|
return { ok: true, cacheDir: finalDir, version: "local", sha: undefined };
|
|
96
167
|
}
|
|
97
168
|
if (source.source === "git" || source.source === "github") {
|
|
@@ -99,32 +170,51 @@ async function materialize(source, marketplaceInstallLocation, marketplace, plug
|
|
|
99
170
|
const r = await materializeGit(url, source.ref, placeholder);
|
|
100
171
|
if (!r.ok) {
|
|
101
172
|
if (existsSync(placeholder))
|
|
102
|
-
|
|
173
|
+
await rm(placeholder, { recursive: true, force: true });
|
|
103
174
|
return r;
|
|
104
175
|
}
|
|
176
|
+
// Supply-chain check: when the marketplace entry pins a SHA, fail the
|
|
177
|
+
// install if the cloned HEAD doesn't match. Without this the `sha`
|
|
178
|
+
// field is decorative — present in metadata, never enforced.
|
|
179
|
+
if (source.sha && !shaMatches(source.sha, r.sha)) {
|
|
180
|
+
if (existsSync(placeholder))
|
|
181
|
+
await rm(placeholder, { recursive: true, force: true });
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: `sha mismatch for ${plugin}: expected ${source.sha}, got ${r.sha}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
105
187
|
const version = shortSha(r.sha);
|
|
106
188
|
const finalDir = pluginCacheDir(marketplace, plugin, version);
|
|
107
189
|
if (existsSync(finalDir))
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
190
|
+
await rm(finalDir, { recursive: true, force: true });
|
|
191
|
+
await mkdir(dirname(finalDir), { recursive: true });
|
|
192
|
+
await cp(placeholder, finalDir, { recursive: true });
|
|
193
|
+
await rm(placeholder, { recursive: true, force: true });
|
|
112
194
|
return { ok: true, cacheDir: finalDir, version, sha: r.sha };
|
|
113
195
|
}
|
|
114
196
|
if (source.source === "git-subdir") {
|
|
115
197
|
const r = await materializeGitSubdir(source.url, source.path, source.ref, placeholder);
|
|
116
198
|
if (!r.ok) {
|
|
117
199
|
if (existsSync(placeholder))
|
|
118
|
-
|
|
200
|
+
await rm(placeholder, { recursive: true, force: true });
|
|
119
201
|
return r;
|
|
120
202
|
}
|
|
203
|
+
if (source.sha && !shaMatches(source.sha, r.sha)) {
|
|
204
|
+
if (existsSync(placeholder))
|
|
205
|
+
await rm(placeholder, { recursive: true, force: true });
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
error: `sha mismatch for ${plugin}: expected ${source.sha}, got ${r.sha}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
121
211
|
const version = shortSha(r.sha);
|
|
122
212
|
const finalDir = pluginCacheDir(marketplace, plugin, version);
|
|
123
213
|
if (existsSync(finalDir))
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
214
|
+
await rm(finalDir, { recursive: true, force: true });
|
|
215
|
+
await mkdir(dirname(finalDir), { recursive: true });
|
|
216
|
+
await cp(placeholder, finalDir, { recursive: true });
|
|
217
|
+
await rm(placeholder, { recursive: true, force: true });
|
|
128
218
|
return { ok: true, cacheDir: finalDir, version, sha: r.sha };
|
|
129
219
|
}
|
|
130
220
|
return { ok: false, error: `unsupported source type "${source.source}"` };
|
|
@@ -194,10 +284,19 @@ export function uninstallPlugin(pluginName, marketplaceName) {
|
|
|
194
284
|
const data = readInstalledPlugins();
|
|
195
285
|
const entries = data.plugins[key];
|
|
196
286
|
let removedFromDisk = false;
|
|
287
|
+
const cacheRoot = pluginCacheRoot();
|
|
197
288
|
if (entries) {
|
|
198
289
|
for (const e of entries) {
|
|
199
|
-
if (e.installPath
|
|
200
|
-
|
|
290
|
+
if (!e.installPath)
|
|
291
|
+
continue;
|
|
292
|
+
// Containment check: the on-disk installed_plugins.json can be tampered
|
|
293
|
+
// with to point at arbitrary paths (e.g. "/", "$HOME"). Refuse to rm
|
|
294
|
+
// anything that doesn't realpath to a strict child of the plugin cache.
|
|
295
|
+
const safePath = resolveSafePluginPath(e.installPath, cacheRoot);
|
|
296
|
+
if (!safePath)
|
|
297
|
+
continue;
|
|
298
|
+
if (existsSync(safePath)) {
|
|
299
|
+
rmSync(safePath, { recursive: true, force: true });
|
|
201
300
|
removedFromDisk = true;
|
|
202
301
|
}
|
|
203
302
|
}
|
package/dist/plugins/schemas.js
CHANGED
|
@@ -44,6 +44,30 @@ export function validatePluginEntrySource(raw, path) {
|
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
if (kind === "local") {
|
|
48
|
+
// Codex / agents-format marketplaces (e.g. awesome-codex-plugins) express
|
|
49
|
+
// a local plugin as { source: "local", path }. Our installer's local path
|
|
50
|
+
// source is a bare string, so normalize onto that.
|
|
51
|
+
if (typeof raw.path !== "string" || !raw.path) {
|
|
52
|
+
return { ok: false, error: `${path}: local source requires path` };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, value: raw.path };
|
|
55
|
+
}
|
|
56
|
+
if (kind === "url") {
|
|
57
|
+
// The official claude-plugins-official marketplace emits a "url" source
|
|
58
|
+
// type for "clone this git repo" (optionally a subdir via `path`). It is
|
|
59
|
+
// semantically identical to our git / git-subdir sources, so normalize it
|
|
60
|
+
// onto those rather than carrying a fourth code path through the installer.
|
|
61
|
+
if (typeof raw.url !== "string" || !raw.url) {
|
|
62
|
+
return { ok: false, error: `${path}: url source requires url` };
|
|
63
|
+
}
|
|
64
|
+
const ref = typeof raw.ref === "string" ? raw.ref : undefined;
|
|
65
|
+
const sha = typeof raw.sha === "string" ? raw.sha : undefined;
|
|
66
|
+
if (typeof raw.path === "string" && raw.path) {
|
|
67
|
+
return { ok: true, value: { source: "git-subdir", url: raw.url, path: raw.path, ref, sha } };
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, value: { source: "git", url: raw.url, ref, sha } };
|
|
70
|
+
}
|
|
47
71
|
if (kind === "git-subdir") {
|
|
48
72
|
if (typeof raw.url !== "string" || !raw.url) {
|
|
49
73
|
return { ok: false, error: `${path}: git-subdir source requires url` };
|
|
@@ -108,8 +132,21 @@ export function validateMarketplace(raw) {
|
|
|
108
132
|
if (typeof raw.name !== "string" || !raw.name) {
|
|
109
133
|
return { ok: false, error: "marketplace.json: name is required" };
|
|
110
134
|
}
|
|
111
|
-
|
|
112
|
-
|
|
135
|
+
// Claude Code marketplaces carry a top-level `owner: { name }`. Codex /
|
|
136
|
+
// agents-format marketplaces omit it and instead carry `interface.displayName`.
|
|
137
|
+
// Accept either, falling back to the marketplace name as a last resort, so a
|
|
138
|
+
// Codex manifest is not rejected purely for lacking an owner.
|
|
139
|
+
let ownerName;
|
|
140
|
+
let ownerEmail;
|
|
141
|
+
if (isObject(raw.owner) && typeof raw.owner.name === "string") {
|
|
142
|
+
ownerName = raw.owner.name;
|
|
143
|
+
ownerEmail = typeof raw.owner.email === "string" ? raw.owner.email : undefined;
|
|
144
|
+
}
|
|
145
|
+
else if (isObject(raw.interface) && typeof raw.interface.displayName === "string") {
|
|
146
|
+
ownerName = raw.interface.displayName;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
ownerName = raw.name;
|
|
113
150
|
}
|
|
114
151
|
if (!Array.isArray(raw.plugins)) {
|
|
115
152
|
return { ok: false, error: "marketplace.json: plugins must be an array" };
|
|
@@ -126,10 +163,7 @@ export function validateMarketplace(raw) {
|
|
|
126
163
|
value: {
|
|
127
164
|
name: raw.name,
|
|
128
165
|
description: typeof raw.description === "string" ? raw.description : undefined,
|
|
129
|
-
owner: {
|
|
130
|
-
name: raw.owner.name,
|
|
131
|
-
email: typeof raw.owner.email === "string" ? raw.owner.email : undefined,
|
|
132
|
-
},
|
|
166
|
+
owner: { name: ownerName, email: ownerEmail },
|
|
133
167
|
plugins,
|
|
134
168
|
},
|
|
135
169
|
};
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -11,10 +11,14 @@ export type MarketplaceSource = {
|
|
|
11
11
|
source: "git";
|
|
12
12
|
url: string;
|
|
13
13
|
};
|
|
14
|
+
/** Which plugin-manifest convention a cloned marketplace ships. */
|
|
15
|
+
export type MarketplaceFormat = "claude-code" | "codex" | "universal";
|
|
14
16
|
export interface KnownMarketplace {
|
|
15
17
|
source: MarketplaceSource;
|
|
16
18
|
installLocation: string;
|
|
17
19
|
lastUpdated: string;
|
|
20
|
+
/** Optional: absent on entries written before format detection existed. */
|
|
21
|
+
format?: MarketplaceFormat;
|
|
18
22
|
}
|
|
19
23
|
export type KnownMarketplaces = Record<string, KnownMarketplace>;
|
|
20
24
|
export type PluginEntrySource = string | {
|
package/dist/preset/index.js
CHANGED
|
@@ -20,6 +20,7 @@ const GENERAL_BUILTIN_TOOLS = [
|
|
|
20
20
|
"Bash",
|
|
21
21
|
"WebSearch",
|
|
22
22
|
"WebFetch",
|
|
23
|
+
"GenerateImage",
|
|
23
24
|
"AskUserQuestion",
|
|
24
25
|
"Agent",
|
|
25
26
|
// AgentStatus removed: background agents now write to ~/.code-shell/agents/
|
|
@@ -42,6 +43,17 @@ const GENERAL_BUILTIN_TOOLS = [
|
|
|
42
43
|
"RemoteTrigger",
|
|
43
44
|
"REPL",
|
|
44
45
|
"PowerShell",
|
|
46
|
+
// Persistent cross-session memory. Including these in the default preset is
|
|
47
|
+
// what lets the LLM list/read/save/delete memories during a normal session,
|
|
48
|
+
// AND what makes the end-of-session auto-dream consolidation work — its
|
|
49
|
+
// tool-call loop pulls these from the registry, so without them every dream
|
|
50
|
+
// run bailed with "missing memory tools". Save/Delete in the user scope stay
|
|
51
|
+
// permission-gated (the tools declare permissionDefault: "ask"); dream-scope
|
|
52
|
+
// writes go through freely.
|
|
53
|
+
"MemoryList",
|
|
54
|
+
"MemoryRead",
|
|
55
|
+
"MemorySave",
|
|
56
|
+
"MemoryDelete",
|
|
45
57
|
];
|
|
46
58
|
const TERMINAL_CODING_EXTRA_TOOLS = [
|
|
47
59
|
"EnterWorktree",
|
|
@@ -71,6 +83,10 @@ const GENERAL_PERMISSION_RULES = [
|
|
|
71
83
|
{ tool: "Skill", decision: "allow" },
|
|
72
84
|
{ tool: "ListMcpResources", decision: "allow" },
|
|
73
85
|
{ tool: "ReadMcpResource", decision: "allow" },
|
|
86
|
+
// Reading memory is always safe; writes (MemorySave/MemoryDelete) stay gated
|
|
87
|
+
// by the tools' own permissionDefault so the user confirms each change.
|
|
88
|
+
{ tool: "MemoryList", decision: "allow" },
|
|
89
|
+
{ tool: "MemoryRead", decision: "allow" },
|
|
74
90
|
];
|
|
75
91
|
// ─── Preset definitions ──────────────────────────────────────────
|
|
76
92
|
export const BUILTIN_AGENT_PRESETS = {
|
|
@@ -16,6 +16,10 @@ export interface ComposerOptions {
|
|
|
16
16
|
preset?: AgentPreset;
|
|
17
17
|
customSystemPrompt?: string;
|
|
18
18
|
appendSystemPrompt?: string;
|
|
19
|
+
/** User's preferred response language (free text), injected as a stable system section. */
|
|
20
|
+
responseLanguage?: string;
|
|
21
|
+
/** How to address the user / short profile (free text). */
|
|
22
|
+
userProfile?: string;
|
|
19
23
|
/**
|
|
20
24
|
* Skill names (full names including any "<plugin>:" prefix) the user
|
|
21
25
|
* has disabled in settings. Filtered out of the LLM's skills listing
|
|
@@ -23,6 +27,12 @@ export interface ComposerOptions {
|
|
|
23
27
|
* dispatch — see scanSkills(opts.disabledSkills) and skillTool.
|
|
24
28
|
*/
|
|
25
29
|
disabledSkills?: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Plugin names the user has totally disabled. Coarser knob than
|
|
32
|
+
* disabledSkills — every skill whose namespaced name starts with
|
|
33
|
+
* `${pluginName}:` is filtered. See scanSkills(opts.disabledPlugins).
|
|
34
|
+
*/
|
|
35
|
+
disabledPlugins?: string[];
|
|
26
36
|
}
|
|
27
37
|
export declare class PromptComposer {
|
|
28
38
|
private readonly options;
|
package/dist/prompt/composer.js
CHANGED
|
@@ -34,7 +34,11 @@ export class PromptComposer {
|
|
|
34
34
|
const memoryContext = this.getMemoryContext();
|
|
35
35
|
if (!instructions && !memoryContext)
|
|
36
36
|
return null;
|
|
37
|
-
|
|
37
|
+
// Local date, not UTC — toISOString() is UTC and would show the wrong
|
|
38
|
+
// "today" for users whose timezone has crossed midnight (review-2026-05-30).
|
|
39
|
+
const now = new Date();
|
|
40
|
+
const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
41
|
+
const currentDate = `Today's date is ${ymd}.`;
|
|
38
42
|
let content = `<system-reminder>\n${currentDate}\n\n`;
|
|
39
43
|
if (instructions)
|
|
40
44
|
content += `${instructions}\n\n`;
|
|
@@ -111,13 +115,16 @@ export class PromptComposer {
|
|
|
111
115
|
compute: () => this.options.customSystemPrompt,
|
|
112
116
|
});
|
|
113
117
|
}
|
|
114
|
-
// Tool
|
|
118
|
+
// Tool listing — name + one-line description only. The full JSON schema
|
|
119
|
+
// is NOT repeated here: the provider clients already send it in the
|
|
120
|
+
// native `tools` field (Anthropic tools / OpenAI functions), so dumping
|
|
121
|
+
// `Parameters: {...}` into the system prompt sent every tool's schema
|
|
122
|
+
// twice — a large, per-request token cost for no added model signal.
|
|
115
123
|
if (tools.length > 0) {
|
|
116
124
|
sections.push({
|
|
117
125
|
name: "tool_definitions",
|
|
118
126
|
compute: () => {
|
|
119
|
-
const toolLines = tools.map((t) => `### ${t.name}\n${t.description}
|
|
120
|
-
`Parameters: ${JSON.stringify(t.inputSchema, null, 2)}`);
|
|
127
|
+
const toolLines = tools.map((t) => `### ${t.name}\n${t.description}`);
|
|
121
128
|
return `# Available Tools\n\n${toolLines.join("\n\n")}`;
|
|
122
129
|
},
|
|
123
130
|
});
|
|
@@ -136,6 +143,7 @@ export class PromptComposer {
|
|
|
136
143
|
compute: () => {
|
|
137
144
|
const skills = scanSkills(this.options.cwd, {
|
|
138
145
|
disabledSkills: this.options.disabledSkills,
|
|
146
|
+
disabledPlugins: this.options.disabledPlugins,
|
|
139
147
|
});
|
|
140
148
|
return buildSkillListing(skills);
|
|
141
149
|
},
|
|
@@ -147,6 +155,23 @@ export class PromptComposer {
|
|
|
147
155
|
compute: () => this.options.appendSystemPrompt,
|
|
148
156
|
});
|
|
149
157
|
}
|
|
158
|
+
// Personalization — stable user preferences (language + how to address
|
|
159
|
+
// the user). Placed in the cacheable system prefix because it doesn't
|
|
160
|
+
// change per-turn. Only emitted when at least one field is set.
|
|
161
|
+
const { responseLanguage, userProfile } = this.options;
|
|
162
|
+
if (responseLanguage || userProfile) {
|
|
163
|
+
sections.push({
|
|
164
|
+
name: "personalization",
|
|
165
|
+
compute: () => {
|
|
166
|
+
const lines = ["# User & Response Preferences"];
|
|
167
|
+
if (userProfile)
|
|
168
|
+
lines.push(`- About the user: ${userProfile}`);
|
|
169
|
+
if (responseLanguage)
|
|
170
|
+
lines.push(`- Response language: ${responseLanguage}`);
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
150
175
|
return sections;
|
|
151
176
|
}
|
|
152
177
|
getInstructions() {
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
- Avoid creating files unless they are genuinely needed. Prefer editing existing files to creating new ones.
|
|
7
7
|
- Avoid giving time estimates or predictions for how long tasks will take.
|
|
8
8
|
|
|
9
|
+
# Delegating to sub-agents (Agent)
|
|
10
|
+
- A sub-agent runs in its own **clean, isolated context** and returns only its final report. It is stateless — you cannot send follow-up messages, so its prompt must be a complete, self-contained task description.
|
|
11
|
+
- **The primary reason to delegate is context hygiene, not speed.** When a task needs to read many files or run a long exploration but you only need the *conclusion*, hand it to a sub-agent: it does the noisy work in its own context and you keep the answer, not the file dumps. This is the default move for any sweep that would otherwise flood your own context with intermediate output.
|
|
12
|
+
- **Parallel fan-out is the exception, not the default.** Only launch several sub-agents at once when the work genuinely splits into independent pieces with no shared state or ordering. Don't open multiple agents just because a task *could* be subdivided — one well-scoped delegation usually beats a swarm.
|
|
13
|
+
- **Delegate the dirty work, keep the thinking.** Push file-reading, searching, and broad investigation into sub-agents; do the synthesis, decisions, and edits yourself in the main thread.
|
|
14
|
+
- **Don't delegate trivial lookups.** If you know the file/symbol/value and expect a few matches, use Read/Grep/Glob directly — spinning up an agent for that wastes a turn and tokens.
|
|
15
|
+
- If you delegate a chunk of work, add a TodoWrite item for it so the user sees it without reading the sub-agent's transcript.
|
|
16
|
+
|
|
9
17
|
# Task tracking (TodoWrite)
|
|
10
18
|
- When the user's request decomposes into 3+ discrete steps, call **TodoWrite** with the complete list up front. The user has a pinned task panel and relies on it to see what's queued, in progress, and done.
|
|
11
19
|
- **TodoWrite takes the complete list each time** — there is no per-item update. To change a status, call TodoWrite again with the whole list, only that item's `status` changed. Your previous TodoWrite input IS the source of truth — re-read it before rewriting.
|
|
@@ -2,7 +2,7 @@ import { ChatSession } from "./chat-session.js";
|
|
|
2
2
|
import type { Engine } from "../engine/engine.js";
|
|
3
3
|
import type { EngineRuntime } from "../engine/runtime.js";
|
|
4
4
|
import type { EngineConfig } from "../engine/engine.js";
|
|
5
|
-
export type EngineConfigSlice = Pick<EngineConfig, "permissionMode" | "preset" | "customSystemPrompt" | "appendSystemPrompt" | "maxTurns" | "maxContextTokens" | "cwd">;
|
|
5
|
+
export type EngineConfigSlice = Pick<EngineConfig, "permissionMode" | "preset" | "customSystemPrompt" | "appendSystemPrompt" | "goal" | "maxTurns" | "maxContextTokens" | "cwd">;
|
|
6
6
|
export interface ChatSessionManagerOptions {
|
|
7
7
|
/**
|
|
8
8
|
* Shared runtime, supplied here so callers and engineFactory closures
|
|
@@ -24,6 +24,13 @@ export declare class ChatSessionManager {
|
|
|
24
24
|
constructor(opts: ChatSessionManagerOptions);
|
|
25
25
|
getOrCreate(sessionId: string, slice: EngineConfigSlice): ChatSession;
|
|
26
26
|
get(sessionId: string): ChatSession | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Iterate every live session once. Public iterator so callers (e.g. the
|
|
29
|
+
* protocol server's config hot-reload / sessions query) don't reach into
|
|
30
|
+
* the private `sessions` map with an `as any` cast. Iterates a snapshot of
|
|
31
|
+
* the values so a callback that closes a session can't perturb the walk.
|
|
32
|
+
*/
|
|
33
|
+
forEachSession(fn: (s: ChatSession) => void): void;
|
|
27
34
|
close(sessionId: string): void;
|
|
28
35
|
closeAll(): void;
|
|
29
36
|
sessionCount(): number;
|
|
@@ -29,6 +29,16 @@ export class ChatSessionManager {
|
|
|
29
29
|
get(sessionId) {
|
|
30
30
|
return this.sessions.get(sessionId);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Iterate every live session once. Public iterator so callers (e.g. the
|
|
34
|
+
* protocol server's config hot-reload / sessions query) don't reach into
|
|
35
|
+
* the private `sessions` map with an `as any` cast. Iterates a snapshot of
|
|
36
|
+
* the values so a callback that closes a session can't perturb the walk.
|
|
37
|
+
*/
|
|
38
|
+
forEachSession(fn) {
|
|
39
|
+
for (const s of [...this.sessions.values()])
|
|
40
|
+
fn(s);
|
|
41
|
+
}
|
|
32
42
|
close(sessionId) {
|
|
33
43
|
const s = this.sessions.get(sessionId);
|
|
34
44
|
if (!s)
|
|
@@ -10,6 +10,9 @@ export interface TurnOpts {
|
|
|
10
10
|
* configured cwd. */
|
|
11
11
|
cwd?: string;
|
|
12
12
|
onStream?: (event: StreamEvent) => void;
|
|
13
|
+
/** Goal mode for this turn — forwarded to engine.run (loop-until-done).
|
|
14
|
+
* String objective or full GoalConfig (objective + optional budgets). */
|
|
15
|
+
goal?: string | import("../engine/goal.js").GoalConfig;
|
|
13
16
|
}
|
|
14
17
|
/**
|
|
15
18
|
* One ChatSession per UI chat tab. Owns a single Engine, an AbortController
|
|
@@ -32,6 +35,13 @@ export declare class ChatSession {
|
|
|
32
35
|
private active;
|
|
33
36
|
private controller;
|
|
34
37
|
private readonly defaultOnStream?;
|
|
38
|
+
/**
|
|
39
|
+
* Model-pool key requested via requestModelSwitch while a turn was in
|
|
40
|
+
* flight. Applied at the next run boundary (before the next turn starts) so
|
|
41
|
+
* we never swap the model out from under a running LLM client — the bug the
|
|
42
|
+
* session-isolation research flagged. null = nothing pending.
|
|
43
|
+
*/
|
|
44
|
+
private pendingModel;
|
|
35
45
|
constructor(opts: ChatSessionOptions);
|
|
36
46
|
enqueueTurn(task: string, opts: TurnOpts): Promise<EngineResult>;
|
|
37
47
|
/**
|
|
@@ -44,6 +54,13 @@ export declare class ChatSession {
|
|
|
44
54
|
*/
|
|
45
55
|
cancel(): void;
|
|
46
56
|
isBusy(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Switch this session's active model (per-session, not worker-global).
|
|
59
|
+
* Applies immediately when idle; defers to the next run boundary when a
|
|
60
|
+
* turn is in flight so a hot switch never mutates the model under a
|
|
61
|
+
* running LLM client.
|
|
62
|
+
*/
|
|
63
|
+
requestModelSwitch(key: string): void;
|
|
47
64
|
queueDepth(): number;
|
|
48
65
|
private pump;
|
|
49
66
|
}
|
|
@@ -19,6 +19,13 @@ export class ChatSession {
|
|
|
19
19
|
active = null;
|
|
20
20
|
controller = null;
|
|
21
21
|
defaultOnStream;
|
|
22
|
+
/**
|
|
23
|
+
* Model-pool key requested via requestModelSwitch while a turn was in
|
|
24
|
+
* flight. Applied at the next run boundary (before the next turn starts) so
|
|
25
|
+
* we never swap the model out from under a running LLM client — the bug the
|
|
26
|
+
* session-isolation research flagged. null = nothing pending.
|
|
27
|
+
*/
|
|
28
|
+
pendingModel = null;
|
|
22
29
|
constructor(opts) {
|
|
23
30
|
this.id = opts.id;
|
|
24
31
|
this.engine = opts.engine;
|
|
@@ -50,6 +57,19 @@ export class ChatSession {
|
|
|
50
57
|
isBusy() {
|
|
51
58
|
return this.active !== null;
|
|
52
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Switch this session's active model (per-session, not worker-global).
|
|
62
|
+
* Applies immediately when idle; defers to the next run boundary when a
|
|
63
|
+
* turn is in flight so a hot switch never mutates the model under a
|
|
64
|
+
* running LLM client.
|
|
65
|
+
*/
|
|
66
|
+
requestModelSwitch(key) {
|
|
67
|
+
if (this.isBusy()) {
|
|
68
|
+
this.pendingModel = key;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.engine.switchModel(key);
|
|
72
|
+
}
|
|
53
73
|
queueDepth() {
|
|
54
74
|
return this.queue.length;
|
|
55
75
|
}
|
|
@@ -68,6 +88,7 @@ export class ChatSession {
|
|
|
68
88
|
sessionId: this.id,
|
|
69
89
|
signal: this.controller.signal,
|
|
70
90
|
onStream,
|
|
91
|
+
goal: next.opts.goal,
|
|
71
92
|
});
|
|
72
93
|
this.lastActivityAt = Date.now();
|
|
73
94
|
next.resolve(result);
|
|
@@ -78,6 +99,13 @@ export class ChatSession {
|
|
|
78
99
|
finally {
|
|
79
100
|
this.active = null;
|
|
80
101
|
this.controller = null;
|
|
102
|
+
// Run boundary: apply any model switch that was deferred because it was
|
|
103
|
+
// requested mid-run. Done here (not in pump) so it still applies when
|
|
104
|
+
// no further turn is queued.
|
|
105
|
+
if (this.pendingModel !== null) {
|
|
106
|
+
this.engine.switchModel(this.pendingModel);
|
|
107
|
+
this.pendingModel = null;
|
|
108
|
+
}
|
|
81
109
|
// Drain the next turn if one is waiting.
|
|
82
110
|
if (this.queue.length > 0)
|
|
83
111
|
void this.pump();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Produce a short preview (`sk-…abcd`) so UIs can render presence without revealing the secret. */
|
|
2
|
+
export declare function makeApiKeyPreview(apiKey: string | undefined | null): string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Return true when a dotted setting key path points at a secret field.
|
|
5
|
+
* Used to decide whether `config_get`'s response should redact the value.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isSecretKeyPath(key: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Redact a single value returned by config_get.
|
|
10
|
+
*
|
|
11
|
+
* Two-stage rule:
|
|
12
|
+
* 1. If the caller-supplied key path itself looks secret
|
|
13
|
+
* ("llm.apiKey", "headers.authorization", …) → collapse to "[redacted]".
|
|
14
|
+
* 2. Otherwise, if the value is an object/array (e.g. config_get("llm") or
|
|
15
|
+
* config_get("providers")), walk it with the shared secret redactor
|
|
16
|
+
* from logging/sanitize-messages so nested apiKey / token / Bearer
|
|
17
|
+
* fields don't slip through verbatim. Primitives pass unchanged.
|
|
18
|
+
*
|
|
19
|
+
* Pre-fix this only inspected the top-level key string, so
|
|
20
|
+
* config_get("providers") returned an array of provider objects with raw
|
|
21
|
+
* apiKey values intact. That defeated the purpose of the boundary.
|
|
22
|
+
*/
|
|
23
|
+
export declare function maskSecretValue<T>(key: string, value: T): T | "[redacted]";
|
|
24
|
+
/**
|
|
25
|
+
* Shape of the LLM block returned to clients. We deliberately omit `apiKey`
|
|
26
|
+
* and surface presence/preview instead. Callers that need the raw key already
|
|
27
|
+
* have it server-side; clients should never see it.
|
|
28
|
+
*/
|
|
29
|
+
export interface RedactedLlmConfig {
|
|
30
|
+
provider?: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
baseUrl?: string;
|
|
33
|
+
maxTokens?: number;
|
|
34
|
+
/** True when an apiKey is configured. */
|
|
35
|
+
hasApiKey: boolean;
|
|
36
|
+
/** Short preview when an apiKey is configured; omitted otherwise. */
|
|
37
|
+
apiKeyPreview?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Rewrite the LLM section of a config snapshot for transport. We only forward
|
|
41
|
+
* fields the client legitimately needs to render status; secrets become
|
|
42
|
+
* derived flags.
|
|
43
|
+
*/
|
|
44
|
+
export declare function redactLlmConfig(llm: {
|
|
45
|
+
provider?: string;
|
|
46
|
+
model?: string;
|
|
47
|
+
apiKey?: string;
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
maxTokens?: number;
|
|
50
|
+
}): RedactedLlmConfig;
|