@chances-ai/engine 24.0.0
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/agents/discover.d.ts +30 -0
- package/dist/agents/discover.d.ts.map +1 -0
- package/dist/agents/discover.js +183 -0
- package/dist/agents/discover.js.map +1 -0
- package/dist/agents/index.d.ts +20 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +52 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/parse.d.ts +61 -0
- package/dist/agents/parse.d.ts.map +1 -0
- package/dist/agents/parse.js +527 -0
- package/dist/agents/parse.js.map +1 -0
- package/dist/agents/types.d.ts +52 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +8 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/ai/adapters/ai-sdk-stream.d.ts +19 -0
- package/dist/ai/adapters/ai-sdk-stream.d.ts.map +1 -0
- package/dist/ai/adapters/ai-sdk-stream.js +125 -0
- package/dist/ai/adapters/ai-sdk-stream.js.map +1 -0
- package/dist/ai/adapters/ai-sdk.d.ts +56 -0
- package/dist/ai/adapters/ai-sdk.d.ts.map +1 -0
- package/dist/ai/adapters/ai-sdk.js +112 -0
- package/dist/ai/adapters/ai-sdk.js.map +1 -0
- package/dist/ai/adapters/mock.d.ts +13 -0
- package/dist/ai/adapters/mock.d.ts.map +1 -0
- package/dist/ai/adapters/mock.js +54 -0
- package/dist/ai/adapters/mock.js.map +1 -0
- package/dist/ai/adapters/openai-compatible.d.ts +23 -0
- package/dist/ai/adapters/openai-compatible.d.ts.map +1 -0
- package/dist/ai/adapters/openai-compatible.js +45 -0
- package/dist/ai/adapters/openai-compatible.js.map +1 -0
- package/dist/ai/cost.d.ts +3 -0
- package/dist/ai/cost.d.ts.map +1 -0
- package/dist/ai/cost.js +5 -0
- package/dist/ai/cost.js.map +1 -0
- package/dist/ai/index.d.ts +12 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +11 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/known-models.d.ts +20 -0
- package/dist/ai/known-models.d.ts.map +1 -0
- package/dist/ai/known-models.js +129 -0
- package/dist/ai/known-models.js.map +1 -0
- package/dist/ai/registry.d.ts +12 -0
- package/dist/ai/registry.d.ts.map +1 -0
- package/dist/ai/registry.js +24 -0
- package/dist/ai/registry.js.map +1 -0
- package/dist/ai/retry.d.ts +11 -0
- package/dist/ai/retry.d.ts.map +1 -0
- package/dist/ai/retry.js +14 -0
- package/dist/ai/retry.js.map +1 -0
- package/dist/ai/router.d.ts +25 -0
- package/dist/ai/router.d.ts.map +1 -0
- package/dist/ai/router.js +36 -0
- package/dist/ai/router.js.map +1 -0
- package/dist/ai/setup.d.ts +23 -0
- package/dist/ai/setup.d.ts.map +1 -0
- package/dist/ai/setup.js +47 -0
- package/dist/ai/setup.js.map +1 -0
- package/dist/ai/summarizer.d.ts +24 -0
- package/dist/ai/summarizer.d.ts.map +1 -0
- package/dist/ai/summarizer.js +56 -0
- package/dist/ai/summarizer.js.map +1 -0
- package/dist/ai/types.d.ts +83 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +2 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/core/compaction/circuit-breaker.d.ts +32 -0
- package/dist/core/compaction/circuit-breaker.d.ts.map +1 -0
- package/dist/core/compaction/circuit-breaker.js +42 -0
- package/dist/core/compaction/circuit-breaker.js.map +1 -0
- package/dist/core/compaction/compactor.d.ts +75 -0
- package/dist/core/compaction/compactor.d.ts.map +1 -0
- package/dist/core/compaction/compactor.js +261 -0
- package/dist/core/compaction/compactor.js.map +1 -0
- package/dist/core/compaction/estimate.d.ts +39 -0
- package/dist/core/compaction/estimate.d.ts.map +1 -0
- package/dist/core/compaction/estimate.js +74 -0
- package/dist/core/compaction/estimate.js.map +1 -0
- package/dist/core/compaction/index.d.ts +5 -0
- package/dist/core/compaction/index.d.ts.map +1 -0
- package/dist/core/compaction/index.js +5 -0
- package/dist/core/compaction/index.js.map +1 -0
- package/dist/core/compaction/prune.d.ts +43 -0
- package/dist/core/compaction/prune.d.ts.map +1 -0
- package/dist/core/compaction/prune.js +51 -0
- package/dist/core/compaction/prune.js.map +1 -0
- package/dist/core/engine.d.ts +268 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +767 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/task-tool.d.ts +175 -0
- package/dist/core/task-tool.d.ts.map +1 -0
- package/dist/core/task-tool.js +901 -0
- package/dist/core/task-tool.js.map +1 -0
- package/dist/core/workspace-query.d.ts +83 -0
- package/dist/core/workspace-query.d.ts.map +1 -0
- package/dist/core/workspace-query.js +217 -0
- package/dist/core/workspace-query.js.map +1 -0
- package/dist/core/worktree/active-marker.d.ts +31 -0
- package/dist/core/worktree/active-marker.d.ts.map +1 -0
- package/dist/core/worktree/active-marker.js +109 -0
- package/dist/core/worktree/active-marker.js.map +1 -0
- package/dist/core/worktree/create.d.ts +40 -0
- package/dist/core/worktree/create.d.ts.map +1 -0
- package/dist/core/worktree/create.js +121 -0
- package/dist/core/worktree/create.js.map +1 -0
- package/dist/core/worktree/errors.d.ts +7 -0
- package/dist/core/worktree/errors.d.ts.map +1 -0
- package/dist/core/worktree/errors.js +11 -0
- package/dist/core/worktree/errors.js.map +1 -0
- package/dist/core/worktree/gc.d.ts +39 -0
- package/dist/core/worktree/gc.d.ts.map +1 -0
- package/dist/core/worktree/gc.js +146 -0
- package/dist/core/worktree/gc.js.map +1 -0
- package/dist/core/worktree/git.d.ts +53 -0
- package/dist/core/worktree/git.d.ts.map +1 -0
- package/dist/core/worktree/git.js +166 -0
- package/dist/core/worktree/git.js.map +1 -0
- package/dist/core/worktree/index.d.ts +8 -0
- package/dist/core/worktree/index.d.ts.map +1 -0
- package/dist/core/worktree/index.js +8 -0
- package/dist/core/worktree/index.js.map +1 -0
- package/dist/core/worktree/paths.d.ts +26 -0
- package/dist/core/worktree/paths.d.ts.map +1 -0
- package/dist/core/worktree/paths.js +57 -0
- package/dist/core/worktree/paths.js.map +1 -0
- package/dist/core/worktree/slug.d.ts +6 -0
- package/dist/core/worktree/slug.d.ts.map +1 -0
- package/dist/core/worktree/slug.js +21 -0
- package/dist/core/worktree/slug.js.map +1 -0
- package/dist/local-vault/file-store.d.ts +64 -0
- package/dist/local-vault/file-store.d.ts.map +1 -0
- package/dist/local-vault/file-store.js +225 -0
- package/dist/local-vault/file-store.js.map +1 -0
- package/dist/local-vault/index.d.ts +57 -0
- package/dist/local-vault/index.d.ts.map +1 -0
- package/dist/local-vault/index.js +68 -0
- package/dist/local-vault/index.js.map +1 -0
- package/dist/local-vault/keychain.d.ts +58 -0
- package/dist/local-vault/keychain.d.ts.map +1 -0
- package/dist/local-vault/keychain.js +200 -0
- package/dist/local-vault/keychain.js.map +1 -0
- package/dist/local-vault/mutex.d.ts +20 -0
- package/dist/local-vault/mutex.d.ts.map +1 -0
- package/dist/local-vault/mutex.js +44 -0
- package/dist/local-vault/mutex.js.map +1 -0
- package/dist/local-vault/passphrase.d.ts +30 -0
- package/dist/local-vault/passphrase.d.ts.map +1 -0
- package/dist/local-vault/passphrase.js +72 -0
- package/dist/local-vault/passphrase.js.map +1 -0
- package/dist/lsp/config.d.ts +34 -0
- package/dist/lsp/config.d.ts.map +1 -0
- package/dist/lsp/config.js +68 -0
- package/dist/lsp/config.js.map +1 -0
- package/dist/lsp/detect.d.ts +7 -0
- package/dist/lsp/detect.d.ts.map +1 -0
- package/dist/lsp/detect.js +78 -0
- package/dist/lsp/detect.js.map +1 -0
- package/dist/lsp/errors.d.ts +11 -0
- package/dist/lsp/errors.d.ts.map +1 -0
- package/dist/lsp/errors.js +11 -0
- package/dist/lsp/errors.js.map +1 -0
- package/dist/lsp/formatters.d.ts +147 -0
- package/dist/lsp/formatters.d.ts.map +1 -0
- package/dist/lsp/formatters.js +259 -0
- package/dist/lsp/formatters.js.map +1 -0
- package/dist/lsp/index.d.ts +31 -0
- package/dist/lsp/index.d.ts.map +1 -0
- package/dist/lsp/index.js +31 -0
- package/dist/lsp/index.js.map +1 -0
- package/dist/lsp/instance.d.ts +72 -0
- package/dist/lsp/instance.d.ts.map +1 -0
- package/dist/lsp/instance.js +489 -0
- package/dist/lsp/instance.js.map +1 -0
- package/dist/lsp/lazy-load.d.ts +27 -0
- package/dist/lsp/lazy-load.d.ts.map +1 -0
- package/dist/lsp/lazy-load.js +57 -0
- package/dist/lsp/lazy-load.js.map +1 -0
- package/dist/lsp/manager.d.ts +59 -0
- package/dist/lsp/manager.d.ts.map +1 -0
- package/dist/lsp/manager.js +242 -0
- package/dist/lsp/manager.js.map +1 -0
- package/dist/lsp/ops.d.ts +13 -0
- package/dist/lsp/ops.d.ts.map +1 -0
- package/dist/lsp/ops.js +225 -0
- package/dist/lsp/ops.js.map +1 -0
- package/dist/lsp/rpc.d.ts +47 -0
- package/dist/lsp/rpc.d.ts.map +1 -0
- package/dist/lsp/rpc.js +134 -0
- package/dist/lsp/rpc.js.map +1 -0
- package/dist/lsp/safe-uri.d.ts +18 -0
- package/dist/lsp/safe-uri.d.ts.map +1 -0
- package/dist/lsp/safe-uri.js +96 -0
- package/dist/lsp/safe-uri.js.map +1 -0
- package/dist/lsp/types.d.ts +70 -0
- package/dist/lsp/types.d.ts.map +1 -0
- package/dist/lsp/types.js +16 -0
- package/dist/lsp/types.js.map +1 -0
- package/dist/mcp/bridge.d.ts +57 -0
- package/dist/mcp/bridge.d.ts.map +1 -0
- package/dist/mcp/bridge.js +98 -0
- package/dist/mcp/bridge.js.map +1 -0
- package/dist/mcp/category.d.ts +22 -0
- package/dist/mcp/category.d.ts.map +1 -0
- package/dist/mcp/category.js +11 -0
- package/dist/mcp/category.js.map +1 -0
- package/dist/mcp/client.d.ts +228 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +352 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/content.d.ts +86 -0
- package/dist/mcp/content.d.ts.map +1 -0
- package/dist/mcp/content.js +147 -0
- package/dist/mcp/content.js.map +1 -0
- package/dist/mcp/env.d.ts +19 -0
- package/dist/mcp/env.d.ts.map +1 -0
- package/dist/mcp/env.js +50 -0
- package/dist/mcp/env.js.map +1 -0
- package/dist/mcp/host.d.ts +199 -0
- package/dist/mcp/host.d.ts.map +1 -0
- package/dist/mcp/host.js +530 -0
- package/dist/mcp/host.js.map +1 -0
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +17 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/load-mcp-host.d.ts +32 -0
- package/dist/mcp/load-mcp-host.d.ts.map +1 -0
- package/dist/mcp/load-mcp-host.js +49 -0
- package/dist/mcp/load-mcp-host.js.map +1 -0
- package/dist/mcp/oauth/callback-server.d.ts +73 -0
- package/dist/mcp/oauth/callback-server.d.ts.map +1 -0
- package/dist/mcp/oauth/callback-server.js +280 -0
- package/dist/mcp/oauth/callback-server.js.map +1 -0
- package/dist/mcp/oauth/config-hash.d.ts +24 -0
- package/dist/mcp/oauth/config-hash.d.ts.map +1 -0
- package/dist/mcp/oauth/config-hash.js +55 -0
- package/dist/mcp/oauth/config-hash.js.map +1 -0
- package/dist/mcp/oauth/error-normalize.d.ts +39 -0
- package/dist/mcp/oauth/error-normalize.d.ts.map +1 -0
- package/dist/mcp/oauth/error-normalize.js +91 -0
- package/dist/mcp/oauth/error-normalize.js.map +1 -0
- package/dist/mcp/oauth/provider.d.ts +190 -0
- package/dist/mcp/oauth/provider.d.ts.map +1 -0
- package/dist/mcp/oauth/provider.js +305 -0
- package/dist/mcp/oauth/provider.js.map +1 -0
- package/dist/mcp/oauth/refresh-coalescer.d.ts +46 -0
- package/dist/mcp/oauth/refresh-coalescer.d.ts.map +1 -0
- package/dist/mcp/oauth/refresh-coalescer.js +77 -0
- package/dist/mcp/oauth/refresh-coalescer.js.map +1 -0
- package/dist/mcp/oauth/sdk-shapes.d.ts +77 -0
- package/dist/mcp/oauth/sdk-shapes.d.ts.map +1 -0
- package/dist/mcp/oauth/sdk-shapes.js +21 -0
- package/dist/mcp/oauth/sdk-shapes.js.map +1 -0
- package/dist/mcp/parse.d.ts +28 -0
- package/dist/mcp/parse.d.ts.map +1 -0
- package/dist/mcp/parse.js +209 -0
- package/dist/mcp/parse.js.map +1 -0
- package/dist/mcp/prompts.d.ts +31 -0
- package/dist/mcp/prompts.d.ts.map +1 -0
- package/dist/mcp/prompts.js +71 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/redact.d.ts +62 -0
- package/dist/mcp/redact.d.ts.map +1 -0
- package/dist/mcp/redact.js +87 -0
- package/dist/mcp/redact.js.map +1 -0
- package/dist/mcp/resources.d.ts +70 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +170 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/types.d.ts +123 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/memory/frontmatter.d.ts +18 -0
- package/dist/memory/frontmatter.d.ts.map +1 -0
- package/dist/memory/frontmatter.js +81 -0
- package/dist/memory/frontmatter.js.map +1 -0
- package/dist/memory/index.d.ts +5 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +5 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/store.d.ts +44 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +237 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/tools.d.ts +11 -0
- package/dist/memory/tools.d.ts.map +1 -0
- package/dist/memory/tools.js +159 -0
- package/dist/memory/tools.js.map +1 -0
- package/dist/memory/types.d.ts +32 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +20 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/plugin-api/index.d.ts +167 -0
- package/dist/plugin-api/index.d.ts.map +1 -0
- package/dist/plugin-api/index.js +151 -0
- package/dist/plugin-api/index.js.map +1 -0
- package/dist/plugin-logger/index.d.ts +21 -0
- package/dist/plugin-logger/index.d.ts.map +1 -0
- package/dist/plugin-logger/index.js +59 -0
- package/dist/plugin-logger/index.js.map +1 -0
- package/dist/session/index.d.ts +125 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +202 -0
- package/dist/session/index.js.map +1 -0
- package/dist/tools/approval.d.ts +33 -0
- package/dist/tools/approval.d.ts.map +1 -0
- package/dist/tools/approval.js +53 -0
- package/dist/tools/approval.js.map +1 -0
- package/dist/tools/builtins/_shared.d.ts +94 -0
- package/dist/tools/builtins/_shared.d.ts.map +1 -0
- package/dist/tools/builtins/_shared.js +246 -0
- package/dist/tools/builtins/_shared.js.map +1 -0
- package/dist/tools/builtins/ask-user-question.d.ts +27 -0
- package/dist/tools/builtins/ask-user-question.d.ts.map +1 -0
- package/dist/tools/builtins/ask-user-question.js +191 -0
- package/dist/tools/builtins/ask-user-question.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts +3 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -0
- package/dist/tools/builtins/bash.js +158 -0
- package/dist/tools/builtins/bash.js.map +1 -0
- package/dist/tools/builtins/diff.d.ts +3 -0
- package/dist/tools/builtins/diff.d.ts.map +1 -0
- package/dist/tools/builtins/diff.js +83 -0
- package/dist/tools/builtins/diff.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts +3 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -0
- package/dist/tools/builtins/edit.js +40 -0
- package/dist/tools/builtins/edit.js.map +1 -0
- package/dist/tools/builtins/glob.d.ts +3 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -0
- package/dist/tools/builtins/glob.js +37 -0
- package/dist/tools/builtins/glob.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts +3 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -0
- package/dist/tools/builtins/grep.js +81 -0
- package/dist/tools/builtins/grep.js.map +1 -0
- package/dist/tools/builtins/lsp.d.ts +3 -0
- package/dist/tools/builtins/lsp.d.ts.map +1 -0
- package/dist/tools/builtins/lsp.js +102 -0
- package/dist/tools/builtins/lsp.js.map +1 -0
- package/dist/tools/builtins/pty.d.ts +64 -0
- package/dist/tools/builtins/pty.d.ts.map +1 -0
- package/dist/tools/builtins/pty.js +536 -0
- package/dist/tools/builtins/pty.js.map +1 -0
- package/dist/tools/builtins/read.d.ts +3 -0
- package/dist/tools/builtins/read.d.ts.map +1 -0
- package/dist/tools/builtins/read.js +18 -0
- package/dist/tools/builtins/read.js.map +1 -0
- package/dist/tools/builtins/web-fetch.d.ts +4 -0
- package/dist/tools/builtins/web-fetch.d.ts.map +1 -0
- package/dist/tools/builtins/web-fetch.js +353 -0
- package/dist/tools/builtins/web-fetch.js.map +1 -0
- package/dist/tools/builtins/write.d.ts +3 -0
- package/dist/tools/builtins/write.d.ts.map +1 -0
- package/dist/tools/builtins/write.js +48 -0
- package/dist/tools/builtins/write.js.map +1 -0
- package/dist/tools/builtins.d.ts +9 -0
- package/dist/tools/builtins.d.ts.map +1 -0
- package/dist/tools/builtins.js +29 -0
- package/dist/tools/builtins.js.map +1 -0
- package/dist/tools/diff.d.ts +18 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +57 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/permission.d.ts +120 -0
- package/dist/tools/permission.d.ts.map +1 -0
- package/dist/tools/permission.js +208 -0
- package/dist/tools/permission.js.map +1 -0
- package/dist/tools/registry.d.ts +12 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +19 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +244 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +15 -0
- package/dist/tools/types.js.map +1 -0
- package/package.json +109 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createId } from "@chances-ai/runtime";
|
|
4
|
+
/** Persists sessions as JSON under <workspaceRoot>/.chances/sessions. */
|
|
5
|
+
export class SessionStore {
|
|
6
|
+
dir;
|
|
7
|
+
constructor(workspaceRoot) {
|
|
8
|
+
this.dir = join(workspaceRoot, ".chances", "sessions");
|
|
9
|
+
mkdirSync(this.dir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
save(state) {
|
|
12
|
+
writeFileSync(join(this.dir, `${state.id}.json`), JSON.stringify(state, null, 2));
|
|
13
|
+
}
|
|
14
|
+
load(id) {
|
|
15
|
+
const path = join(this.dir, `${id}.json`);
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return undefined;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
list() {
|
|
26
|
+
return readdirSync(this.dir)
|
|
27
|
+
.filter((f) => f.endsWith(".json"))
|
|
28
|
+
.flatMap((f) => {
|
|
29
|
+
try {
|
|
30
|
+
return [JSON.parse(readFileSync(join(this.dir, f), "utf8"))];
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Owns the session lifecycle (state machine): create/resume, append turns,
|
|
41
|
+
* flatten to a provider message list, checkpoint and restore.
|
|
42
|
+
*/
|
|
43
|
+
export class SessionManager {
|
|
44
|
+
store;
|
|
45
|
+
state;
|
|
46
|
+
constructor(state, store) {
|
|
47
|
+
this.store = store;
|
|
48
|
+
this.state = state;
|
|
49
|
+
}
|
|
50
|
+
static create(title = "session", store) {
|
|
51
|
+
const now = new Date().toISOString();
|
|
52
|
+
return new SessionManager({ id: createId("ses"), title, createdAt: now, updatedAt: now, turns: [] }, store);
|
|
53
|
+
}
|
|
54
|
+
static resume(id, store) {
|
|
55
|
+
const state = store.load(id);
|
|
56
|
+
return state ? new SessionManager(state, store) : undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* (5.5) Creates a NEW session pre-seeded with a deep clone of `parent`'s
|
|
60
|
+
* completed turns — the seam behind the `task` tool's `context: "fork"` mode.
|
|
61
|
+
*
|
|
62
|
+
* Three invariants make this safe:
|
|
63
|
+
* - **New id, no `SessionStore`.** The child is a distinct session that can
|
|
64
|
+
* never `save()` over the parent's `<id>.json` on disk (mirrors
|
|
65
|
+
* `create("subagent")`, which also omits the store). All mutation stays in
|
|
66
|
+
* the child's own in-memory state.
|
|
67
|
+
* - **Whole turns, never a mid-turn slice.** chances-cli persists an
|
|
68
|
+
* assistant message carrying `tool-call` parts and its `tool` results in
|
|
69
|
+
* one atomic turn (see `engine.ts` end-of-turn `appendTurn`), so copying at
|
|
70
|
+
* turn granularity keeps every `tool-call` paired with its result — the
|
|
71
|
+
* provider's no-orphan-`tool_use` invariant holds for free.
|
|
72
|
+
* - **The session `model` field is NOT carried.** The forked child's model is
|
|
73
|
+
* chosen by the caller (persona override or inherited parent selection),
|
|
74
|
+
* not by the parent session's last `/model` choice. `snapshot()` deep-clones
|
|
75
|
+
* the whole state; we keep only `turns`.
|
|
76
|
+
*/
|
|
77
|
+
static forkFrom(parent, title = "subagent") {
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
return new SessionManager({ id: createId("ses"), title, createdAt: now, updatedAt: now, turns: parent.snapshot().turns }, undefined);
|
|
80
|
+
}
|
|
81
|
+
get id() {
|
|
82
|
+
return this.state.id;
|
|
83
|
+
}
|
|
84
|
+
/** All messages across turns, ready to send to a provider. */
|
|
85
|
+
messages() {
|
|
86
|
+
return this.state.turns.flatMap((t) => t.messages);
|
|
87
|
+
}
|
|
88
|
+
appendTurn(messages) {
|
|
89
|
+
const turn = { turnId: createId("turn"), at: new Date().toISOString(), messages };
|
|
90
|
+
this.state.turns.push(turn);
|
|
91
|
+
this.state.updatedAt = turn.at;
|
|
92
|
+
this.store?.save(this.state);
|
|
93
|
+
return turn;
|
|
94
|
+
}
|
|
95
|
+
/** Records the user's current model choice so `/resume` can restore it.
|
|
96
|
+
* Persisted immediately; not gated behind the next `appendTurn`. */
|
|
97
|
+
setModel(model) {
|
|
98
|
+
this.state.model = model;
|
|
99
|
+
this.state.updatedAt = new Date().toISOString();
|
|
100
|
+
this.store?.save(this.state);
|
|
101
|
+
}
|
|
102
|
+
/** Returns the persisted model choice for this session, if any. */
|
|
103
|
+
modelChoice() {
|
|
104
|
+
return this.state.model ? { ...this.state.model } : undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Empties this session's turn list. Used by `/clear` when the user wants a
|
|
108
|
+
* fresh conversation without losing the session metadata (id/title). The
|
|
109
|
+
* underlying file is rewritten so a `/resume` of this id will land on an
|
|
110
|
+
* empty conversation.
|
|
111
|
+
*/
|
|
112
|
+
clearTurns() {
|
|
113
|
+
this.state.turns = [];
|
|
114
|
+
this.state.updatedAt = new Date().toISOString();
|
|
115
|
+
this.store?.save(this.state);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* (3.5) Applies a pure transform to every turn in the prefix
|
|
119
|
+
* (all but the last `keepRecentTurns` turns). Used by the Stage 1
|
|
120
|
+
* pruner — the transform receives one turn's messages, returns
|
|
121
|
+
* the rewritten messages. The turn's `turnId` / `at` are
|
|
122
|
+
* unchanged so persistence keeps its identity; the synthetic
|
|
123
|
+
* `compact-*` turn (from `compactTurns`) is treated like any
|
|
124
|
+
* other turn for indexing purposes.
|
|
125
|
+
*
|
|
126
|
+
* `keepRecentTurns` is clamped to `[0, turns.length]`. When
|
|
127
|
+
* `keepRecentTurns >= turns.length`, this is a no-op (nothing to
|
|
128
|
+
* prune). When 0, every turn including the most recent is fed to
|
|
129
|
+
* the transform.
|
|
130
|
+
*
|
|
131
|
+
* Idempotent if the transform is idempotent (the pruner's marker
|
|
132
|
+
* replacement is — second pass sees the marker, fails the
|
|
133
|
+
* delta>0 check, leaves it alone).
|
|
134
|
+
*/
|
|
135
|
+
pruneTurns(keepRecentTurns, transform) {
|
|
136
|
+
const total = this.state.turns.length;
|
|
137
|
+
const cutoff = Math.max(0, total - Math.max(0, keepRecentTurns));
|
|
138
|
+
if (cutoff === 0)
|
|
139
|
+
return;
|
|
140
|
+
let mutated = false;
|
|
141
|
+
for (let i = 0; i < cutoff; i++) {
|
|
142
|
+
const t = this.state.turns[i];
|
|
143
|
+
const newMessages = transform(t.messages);
|
|
144
|
+
if (newMessages !== t.messages) {
|
|
145
|
+
this.state.turns[i] = { ...t, messages: newMessages };
|
|
146
|
+
mutated = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (mutated) {
|
|
150
|
+
this.state.updatedAt = new Date().toISOString();
|
|
151
|
+
this.store?.save(this.state);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Replaces all but the last `keepLastN` turns with a single synthetic
|
|
156
|
+
* "system context" turn carrying `summary` as a user-role message. Why
|
|
157
|
+
* user-role rather than system-role: providers vary in how they handle
|
|
158
|
+
* mid-conversation system messages, but every provider accepts a long-form
|
|
159
|
+
* user message as historical context. The synthetic turn's `turnId` is
|
|
160
|
+
* tagged `compact-*` so debuggers can spot it.
|
|
161
|
+
*
|
|
162
|
+
* `keepLastN` is clamped to `[0, turns.length]` — passing a larger N keeps
|
|
163
|
+
* everything (compaction is a no-op), passing 0 collapses the whole
|
|
164
|
+
* conversation into the summary. The summary itself must be non-empty;
|
|
165
|
+
* callers should bail before invoking this when the summarizer returned
|
|
166
|
+
* nothing.
|
|
167
|
+
*/
|
|
168
|
+
compactTurns(summary, keepLastN) {
|
|
169
|
+
if (!summary)
|
|
170
|
+
throw new Error("compactTurns: summary must be a non-empty string");
|
|
171
|
+
if (keepLastN < 0)
|
|
172
|
+
throw new Error("compactTurns: keepLastN must be >= 0");
|
|
173
|
+
const total = this.state.turns.length;
|
|
174
|
+
if (keepLastN >= total)
|
|
175
|
+
return; // nothing to compact
|
|
176
|
+
const recent = this.state.turns.slice(total - keepLastN);
|
|
177
|
+
const synthetic = {
|
|
178
|
+
turnId: `compact-${createId("turn")}`,
|
|
179
|
+
at: new Date().toISOString(),
|
|
180
|
+
messages: [
|
|
181
|
+
{
|
|
182
|
+
role: "user",
|
|
183
|
+
content: [{ type: "text", text: `PREVIOUS_CONVERSATION_SUMMARY:\n${summary}` }],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
this.state.turns = [synthetic, ...recent];
|
|
188
|
+
this.state.updatedAt = synthetic.at;
|
|
189
|
+
this.store?.save(this.state);
|
|
190
|
+
}
|
|
191
|
+
checkpoint() {
|
|
192
|
+
return { turnIndex: this.state.turns.length, state: structuredClone(this.state) };
|
|
193
|
+
}
|
|
194
|
+
restore(checkpoint) {
|
|
195
|
+
this.state = structuredClone(checkpoint.state);
|
|
196
|
+
this.store?.save(this.state);
|
|
197
|
+
}
|
|
198
|
+
snapshot() {
|
|
199
|
+
return structuredClone(this.state);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ToolCategory } from "@chances-ai/runtime/config";
|
|
2
|
+
import type { ApprovalMode, ToolTier } from "@chances-ai/runtime";
|
|
3
|
+
/**
|
|
4
|
+
* (5.3) Pure tier logic backing the session approval modes. Lives in
|
|
5
|
+
* `@chances-ai/tools` (next to the gate, its only consumer) rather than in
|
|
6
|
+
* config/runtime so the tables sit beside the code that reads them; the bare
|
|
7
|
+
* `ApprovalMode`/`ToolTier` type unions live in `@chances-ai/runtime` to avoid
|
|
8
|
+
* a config↔runtime import cycle (codex 5.3 Round-1 MUST-FIX #3). See
|
|
9
|
+
* docs/5.3-design.md § 3 (D1/D2/D3).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Each tool category's capability tier. `network-read` (web_fetch) and
|
|
13
|
+
* `integration` (MCP-bridged tools) are **exec**: the first is per-URL-sensitive
|
|
14
|
+
* egress (see `defaultPolicy` rationale in config/types.ts), the second can do
|
|
15
|
+
* anything the server implements. So `auto-edit` prompts both, and `plan`
|
|
16
|
+
* blocks both.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CATEGORY_TIER: Record<ToolCategory, ToolTier>;
|
|
19
|
+
/**
|
|
20
|
+
* True when `mode` blocks the entire `category` outright (plan's read-only
|
|
21
|
+
* floor). The single source of truth shared by `PermissionGate.evaluate()` AND
|
|
22
|
+
* the engine's pre-hook `blockByMode` check — so a `Tool.permission()` that
|
|
23
|
+
* returns `null` to bypass the gate (e.g. `pty.read`) can't sneak past plan
|
|
24
|
+
* mode (codex 5.3 Round-1 MUST-FIX #1).
|
|
25
|
+
*/
|
|
26
|
+
export declare function isBlockedByMode(category: ToolCategory, mode: ApprovalMode): boolean;
|
|
27
|
+
/** True when `mode` auto-approves the entire `category` (yolo / auto-edit),
|
|
28
|
+
* i.e. the interactive prompt is skipped and the call runs. */
|
|
29
|
+
export declare function isAutoApprovedByMode(category: ToolCategory, mode: ApprovalMode): boolean;
|
|
30
|
+
/** Human-readable reason for a mode-driven block, surfaced in the tool result
|
|
31
|
+
* so the model (and user) sees why a call was refused. */
|
|
32
|
+
export declare function modeBlockReason(mode: ApprovalMode): string;
|
|
33
|
+
//# sourceMappingURL=approval.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval.d.ts","sourceRoot":"","sources":["../../src/tools/approval.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;GAOG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,QAAQ,CASxD,CAAC;AAmBF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAEnF;AAED;gEACgE;AAChE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAExF;AAED;2DAC2D;AAC3D,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAE1D"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (5.3) Pure tier logic backing the session approval modes. Lives in
|
|
3
|
+
* `@chances-ai/tools` (next to the gate, its only consumer) rather than in
|
|
4
|
+
* config/runtime so the tables sit beside the code that reads them; the bare
|
|
5
|
+
* `ApprovalMode`/`ToolTier` type unions live in `@chances-ai/runtime` to avoid
|
|
6
|
+
* a config↔runtime import cycle (codex 5.3 Round-1 MUST-FIX #3). See
|
|
7
|
+
* docs/5.3-design.md § 3 (D1/D2/D3).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Each tool category's capability tier. `network-read` (web_fetch) and
|
|
11
|
+
* `integration` (MCP-bridged tools) are **exec**: the first is per-URL-sensitive
|
|
12
|
+
* egress (see `defaultPolicy` rationale in config/types.ts), the second can do
|
|
13
|
+
* anything the server implements. So `auto-edit` prompts both, and `plan`
|
|
14
|
+
* blocks both.
|
|
15
|
+
*/
|
|
16
|
+
export const CATEGORY_TIER = {
|
|
17
|
+
"file-read": "read",
|
|
18
|
+
search: "read",
|
|
19
|
+
"memory-read": "read",
|
|
20
|
+
"file-write": "write",
|
|
21
|
+
"memory-write": "write",
|
|
22
|
+
shell: "exec",
|
|
23
|
+
integration: "exec",
|
|
24
|
+
"network-read": "exec",
|
|
25
|
+
};
|
|
26
|
+
const TIER_RANK = { read: 0, write: 1, exec: 2 };
|
|
27
|
+
const MODE_PROFILE = {
|
|
28
|
+
default: { autoApproveMaxTier: -1, blockAboveTier: 2 }, // pure passthrough to policy
|
|
29
|
+
"auto-edit": { autoApproveMaxTier: 1, blockAboveTier: 2 }, // auto read+write, prompt exec
|
|
30
|
+
plan: { autoApproveMaxTier: -1, blockAboveTier: 0 }, // read-only: block write+exec
|
|
31
|
+
yolo: { autoApproveMaxTier: 2, blockAboveTier: 2 }, // auto-approve every tier
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* True when `mode` blocks the entire `category` outright (plan's read-only
|
|
35
|
+
* floor). The single source of truth shared by `PermissionGate.evaluate()` AND
|
|
36
|
+
* the engine's pre-hook `blockByMode` check — so a `Tool.permission()` that
|
|
37
|
+
* returns `null` to bypass the gate (e.g. `pty.read`) can't sneak past plan
|
|
38
|
+
* mode (codex 5.3 Round-1 MUST-FIX #1).
|
|
39
|
+
*/
|
|
40
|
+
export function isBlockedByMode(category, mode) {
|
|
41
|
+
return TIER_RANK[CATEGORY_TIER[category]] > MODE_PROFILE[mode].blockAboveTier;
|
|
42
|
+
}
|
|
43
|
+
/** True when `mode` auto-approves the entire `category` (yolo / auto-edit),
|
|
44
|
+
* i.e. the interactive prompt is skipped and the call runs. */
|
|
45
|
+
export function isAutoApprovedByMode(category, mode) {
|
|
46
|
+
return TIER_RANK[CATEGORY_TIER[category]] <= MODE_PROFILE[mode].autoApproveMaxTier;
|
|
47
|
+
}
|
|
48
|
+
/** Human-readable reason for a mode-driven block, surfaced in the tool result
|
|
49
|
+
* so the model (and user) sees why a call was refused. */
|
|
50
|
+
export function modeBlockReason(mode) {
|
|
51
|
+
return `Blocked: '${mode}' approval mode is read-only — exit ${mode} mode (shift+tab) to run mutating tools.`;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=approval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../../src/tools/approval.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAmC;IAC3D,WAAW,EAAE,MAAM;IACnB,MAAM,EAAE,MAAM;IACd,aAAa,EAAE,MAAM;IACrB,YAAY,EAAE,OAAO;IACrB,cAAc,EAAE,OAAO;IACvB,KAAK,EAAE,MAAM;IACb,WAAW,EAAE,MAAM;IACnB,cAAc,EAAE,MAAM;CACvB,CAAC;AAEF,MAAM,SAAS,GAA6B,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAU3E,MAAM,YAAY,GAAsC;IACtD,OAAO,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,6BAA6B;IACrF,WAAW,EAAE,EAAE,kBAAkB,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,+BAA+B;IAC1F,IAAI,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,8BAA8B;IACnF,IAAI,EAAE,EAAE,kBAAkB,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,0BAA0B;CAC/E,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,QAAsB,EAAE,IAAkB;IACxE,OAAO,SAAS,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,cAAc,CAAC;AAChF,CAAC;AAED;gEACgE;AAChE,MAAM,UAAU,oBAAoB,CAAC,QAAsB,EAAE,IAAkB;IAC7E,OAAO,SAAS,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,kBAAkB,CAAC;AACrF,CAAC;AAED;2DAC2D;AAC3D,MAAM,UAAU,eAAe,CAAC,IAAkB;IAChD,OAAO,aAAa,IAAI,uCAAuC,IAAI,0CAA0C,CAAC;AAChH,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { JSONValue } from "@chances-ai/runtime";
|
|
2
|
+
import type { ToolContext } from "../types.js";
|
|
3
|
+
/** Diff/write-preview cap: refuses to read either side of a comparison when it
|
|
4
|
+
* exceeds this many bytes, so a `write` against a generated 50 MB file doesn't
|
|
5
|
+
* stall the approval prompt on a synchronous read. */
|
|
6
|
+
export declare const PREVIEW_BYTE_CAP: number;
|
|
7
|
+
export declare function str(args: JSONValue, key: string): string;
|
|
8
|
+
export declare function optStr(args: JSONValue, key: string): string | undefined;
|
|
9
|
+
export declare function optNum(args: JSONValue, key: string): number | undefined;
|
|
10
|
+
export declare function optBool(args: JSONValue, key: string): boolean | undefined;
|
|
11
|
+
/** Resolves a path and refuses to escape the workspace root.
|
|
12
|
+
*
|
|
13
|
+
* Symlinks are resolved through `realpathSync` *before* the containment
|
|
14
|
+
* check whenever EITHER condition holds:
|
|
15
|
+
*
|
|
16
|
+
* - **Isolated context** (`runWithCwd` active — i.e. an
|
|
17
|
+
* `isolation: 'worktree'` subagent): always, so a tracked symlink
|
|
18
|
+
* inside the worktree that points outside it is rejected
|
|
19
|
+
* (4.1 codex Round-1 MUST-FIX #5).
|
|
20
|
+
* - **`opts.resolveSymlinks`** set by the caller: the file-write tools
|
|
21
|
+
* (`write`/`edit`) pass it so that, even in the non-isolated parent
|
|
22
|
+
* context, a workspace-internal symlink pointing OUTSIDE can't be
|
|
23
|
+
* *written through* to plant/modify a file outside the workspace
|
|
24
|
+
* (e.g. a git hook, build script, or dotfile). This closes the
|
|
25
|
+
* write-escape hole that lexical-only containment leaves open — for the
|
|
26
|
+
* full symlink chain (`realpathExtant` walks every hop and fails closed
|
|
27
|
+
* past its depth cap), and `write`/`edit` write to the *resolved* path so
|
|
28
|
+
* the symlink isn't re-traversed after the check.
|
|
29
|
+
*
|
|
30
|
+
* **Residual (out of the single-agent threat model, not closed here):** a
|
|
31
|
+
* classic TOCTOU still exists if a *concurrent* actor swaps a parent dir
|
|
32
|
+
* for an outbound symlink in the window between this check and the
|
|
33
|
+
* `writeFileSync`. Fully closing it needs an `openat`/`O_NOFOLLOW` path
|
|
34
|
+
* walk (no clean Node API); documented as a future-hardening pointer.
|
|
35
|
+
*
|
|
36
|
+
* In the resolving branch, both `workspaceRoot` and the target are
|
|
37
|
+
* realpath'd so the comparison is between fully-resolved trees; for
|
|
38
|
+
* non-existent leaves (the standard `write` case) and dangling symlinks,
|
|
39
|
+
* `realpathExtant` walks the symlink chain up to the nearest existing
|
|
40
|
+
* ancestor and re-joins the missing tail.
|
|
41
|
+
*
|
|
42
|
+
* **Windows junctions.** An NTFS junction (a directory reparse point) is
|
|
43
|
+
* NOT reported as a symlink by `lstat().isSymbolicLink()`, so it skips the
|
|
44
|
+
* manual `readlink` branch in `realpathExtant` — but `realpathSync` DOES
|
|
45
|
+
* traverse it, so a write target under a junction that points outside the
|
|
46
|
+
* workspace still resolves outside and is rejected by the containment check.
|
|
47
|
+
* Covered by the win32-only junction tests in `_shared.symlink.test.ts` (D-C).
|
|
48
|
+
*
|
|
49
|
+
* **Read-class tools** (`read`/`grep`/`glob`/`diff`/`lsp`) deliberately
|
|
50
|
+
* stay lexical in the non-isolated context: reading *through* an inbound
|
|
51
|
+
* symlink (a config/fixture symlinked into a repo) is a common, legitimate
|
|
52
|
+
* pattern, and the high-severity risk is *writing* outside the workspace,
|
|
53
|
+
* not reading a file the caller's own UID can already read. (claude-code
|
|
54
|
+
* resolves reads too; we intentionally keep them lexical here to avoid
|
|
55
|
+
* breaking legit symlinked-file reads — revisit if a read-escape threat
|
|
56
|
+
* emerges.)
|
|
57
|
+
*/
|
|
58
|
+
export declare function safePath(ctx: ToolContext, p: string, opts?: {
|
|
59
|
+
resolveSymlinks?: boolean;
|
|
60
|
+
}): string;
|
|
61
|
+
/** Render a string as a short JSON-quoted preview for permission prompts. */
|
|
62
|
+
export declare function preview(s: string): string;
|
|
63
|
+
export { cleanOutput, normaliseLineEndings, stripAnsi } from "@chances-ai/runtime";
|
|
64
|
+
/** Map an engine call id to a filesystem-safe artifact-dir name (5.7 / codex
|
|
65
|
+
* R1 M2). A clean id (`toolu_…`, `call_…`) is used **verbatim** — that branch is
|
|
66
|
+
* injective and the dir name stays human-readable, joining 1:1 to the
|
|
67
|
+
* `tool_result` the client saw. Anything else — a hostile/unusual provider id
|
|
68
|
+
* containing `/`, `..`, or other bytes, OR an empty string — is replaced
|
|
69
|
+
* WHOLESALE by `id-<sha256[:16]>` rather than lossily mapped (`a/b` and `a_b`
|
|
70
|
+
* must NOT collide into one dir and overwrite each other). That hash branch is
|
|
71
|
+
* **collision-resistant, not strictly injective** (a 64-bit truncation), but
|
|
72
|
+
* for the volume of tool calls in a session the collision probability is
|
|
73
|
+
* negligible and a collision is never a security issue (worst case: two
|
|
74
|
+
* unrelated calls share a dir — never a traversal). Traversal is impossible in
|
|
75
|
+
* both branches: verbatim only admits `[A-Za-z0-9_-]`, the hash only emits hex. */
|
|
76
|
+
export declare function safeArtifactId(raw: string): string;
|
|
77
|
+
/** Best-effort persistence under `<root>/.chances/tool-results/<id>/<file>`
|
|
78
|
+
* with restrictive POSIX permissions (0o700 dir, 0o600 file). Returns the
|
|
79
|
+
* absolute path on success, `null` on filesystem failure (the caller's
|
|
80
|
+
* banner still renders an inline preview either way, so persistence
|
|
81
|
+
* failure degrades to "model only sees the head"; not a fatal tool
|
|
82
|
+
* error).
|
|
83
|
+
*
|
|
84
|
+
* `id` is the engine's `ctx.callId` (5.7) when available — the artifact dir is
|
|
85
|
+
* then named for the real call so a client correlates the saved file with the
|
|
86
|
+
* `tool_result`. Falls back to a fresh `createId(prefix)` for non-engine
|
|
87
|
+
* callers (tests/plugins) — fully backward compatible.
|
|
88
|
+
*
|
|
89
|
+
* **Windows caveat.** Node's `fs` `mode` flag only affects the read-only
|
|
90
|
+
* bit on Windows; ACL-based access control is not enforced via this
|
|
91
|
+
* argument. POSIX-first audience; explicit DACL hardening is a follow-up.
|
|
92
|
+
*/
|
|
93
|
+
export declare function persistFullOutput(workspaceRoot: string, body: string, prefix?: string, filename?: string, id?: string): string | null;
|
|
94
|
+
//# sourceMappingURL=_shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/_shared.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;sDAEsD;AACtD,eAAO,MAAM,gBAAgB,QAAa,CAAC;AAE3C,wBAAgB,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKvE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOvE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAKzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,WAAW,EAChB,CAAC,EAAE,MAAM,EACT,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACnC,MAAM,CAqBR;AA0ED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAGzC;AAaD,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEnF;;;;;;;;;;;mFAWmF;AACnF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,MAAM,EACZ,MAAM,SAAS,EACf,QAAQ,SAAe,EACvB,EAAE,CAAC,EAAE,MAAM,GACV,MAAM,GAAG,IAAI,CAef"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, readlinkSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { AppError, ErrorCode, createId, currentCwdOverride } from "@chances-ai/runtime";
|
|
5
|
+
/** Diff/write-preview cap: refuses to read either side of a comparison when it
|
|
6
|
+
* exceeds this many bytes, so a `write` against a generated 50 MB file doesn't
|
|
7
|
+
* stall the approval prompt on a synchronous read. */
|
|
8
|
+
export const PREVIEW_BYTE_CAP = 256 * 1024;
|
|
9
|
+
export function str(args, key) {
|
|
10
|
+
const v = args[key];
|
|
11
|
+
if (typeof v !== "string")
|
|
12
|
+
throw new AppError(ErrorCode.Tool, `Expected string arg "${key}"`);
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
export function optStr(args, key) {
|
|
16
|
+
const v = args[key];
|
|
17
|
+
if (v === undefined || v === null)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (typeof v !== "string")
|
|
20
|
+
throw new AppError(ErrorCode.Tool, `Expected string arg "${key}"`);
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
export function optNum(args, key) {
|
|
24
|
+
const v = args[key];
|
|
25
|
+
if (v === undefined || v === null)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
28
|
+
throw new AppError(ErrorCode.Tool, `Expected number arg "${key}"`);
|
|
29
|
+
}
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
export function optBool(args, key) {
|
|
33
|
+
const v = args[key];
|
|
34
|
+
if (v === undefined || v === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (typeof v !== "boolean")
|
|
37
|
+
throw new AppError(ErrorCode.Tool, `Expected boolean arg "${key}"`);
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
/** Resolves a path and refuses to escape the workspace root.
|
|
41
|
+
*
|
|
42
|
+
* Symlinks are resolved through `realpathSync` *before* the containment
|
|
43
|
+
* check whenever EITHER condition holds:
|
|
44
|
+
*
|
|
45
|
+
* - **Isolated context** (`runWithCwd` active — i.e. an
|
|
46
|
+
* `isolation: 'worktree'` subagent): always, so a tracked symlink
|
|
47
|
+
* inside the worktree that points outside it is rejected
|
|
48
|
+
* (4.1 codex Round-1 MUST-FIX #5).
|
|
49
|
+
* - **`opts.resolveSymlinks`** set by the caller: the file-write tools
|
|
50
|
+
* (`write`/`edit`) pass it so that, even in the non-isolated parent
|
|
51
|
+
* context, a workspace-internal symlink pointing OUTSIDE can't be
|
|
52
|
+
* *written through* to plant/modify a file outside the workspace
|
|
53
|
+
* (e.g. a git hook, build script, or dotfile). This closes the
|
|
54
|
+
* write-escape hole that lexical-only containment leaves open — for the
|
|
55
|
+
* full symlink chain (`realpathExtant` walks every hop and fails closed
|
|
56
|
+
* past its depth cap), and `write`/`edit` write to the *resolved* path so
|
|
57
|
+
* the symlink isn't re-traversed after the check.
|
|
58
|
+
*
|
|
59
|
+
* **Residual (out of the single-agent threat model, not closed here):** a
|
|
60
|
+
* classic TOCTOU still exists if a *concurrent* actor swaps a parent dir
|
|
61
|
+
* for an outbound symlink in the window between this check and the
|
|
62
|
+
* `writeFileSync`. Fully closing it needs an `openat`/`O_NOFOLLOW` path
|
|
63
|
+
* walk (no clean Node API); documented as a future-hardening pointer.
|
|
64
|
+
*
|
|
65
|
+
* In the resolving branch, both `workspaceRoot` and the target are
|
|
66
|
+
* realpath'd so the comparison is between fully-resolved trees; for
|
|
67
|
+
* non-existent leaves (the standard `write` case) and dangling symlinks,
|
|
68
|
+
* `realpathExtant` walks the symlink chain up to the nearest existing
|
|
69
|
+
* ancestor and re-joins the missing tail.
|
|
70
|
+
*
|
|
71
|
+
* **Windows junctions.** An NTFS junction (a directory reparse point) is
|
|
72
|
+
* NOT reported as a symlink by `lstat().isSymbolicLink()`, so it skips the
|
|
73
|
+
* manual `readlink` branch in `realpathExtant` — but `realpathSync` DOES
|
|
74
|
+
* traverse it, so a write target under a junction that points outside the
|
|
75
|
+
* workspace still resolves outside and is rejected by the containment check.
|
|
76
|
+
* Covered by the win32-only junction tests in `_shared.symlink.test.ts` (D-C).
|
|
77
|
+
*
|
|
78
|
+
* **Read-class tools** (`read`/`grep`/`glob`/`diff`/`lsp`) deliberately
|
|
79
|
+
* stay lexical in the non-isolated context: reading *through* an inbound
|
|
80
|
+
* symlink (a config/fixture symlinked into a repo) is a common, legitimate
|
|
81
|
+
* pattern, and the high-severity risk is *writing* outside the workspace,
|
|
82
|
+
* not reading a file the caller's own UID can already read. (claude-code
|
|
83
|
+
* resolves reads too; we intentionally keep them lexical here to avoid
|
|
84
|
+
* breaking legit symlinked-file reads — revisit if a read-escape threat
|
|
85
|
+
* emerges.)
|
|
86
|
+
*/
|
|
87
|
+
export function safePath(ctx, p, opts) {
|
|
88
|
+
const abs = isAbsolute(p) ? p : resolve(ctx.cwd, p);
|
|
89
|
+
const isolated = currentCwdOverride() !== undefined;
|
|
90
|
+
if (isolated || opts?.resolveSymlinks) {
|
|
91
|
+
// Realpath both sides, then re-check against the fully-resolved trees.
|
|
92
|
+
const rootReal = realpathSafe(ctx.workspaceRoot);
|
|
93
|
+
const absReal = realpathExtant(abs);
|
|
94
|
+
const rel = relative(rootReal, absReal);
|
|
95
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
96
|
+
throw new AppError(ErrorCode.Permission, `Path escapes workspace: ${p}`);
|
|
97
|
+
}
|
|
98
|
+
return absReal;
|
|
99
|
+
}
|
|
100
|
+
// Non-isolated read-class path: lexical containment only, unchanged since 1.x.
|
|
101
|
+
const rel = relative(ctx.workspaceRoot, abs);
|
|
102
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
103
|
+
throw new AppError(ErrorCode.Permission, `Path escapes workspace: ${p}`);
|
|
104
|
+
}
|
|
105
|
+
return abs;
|
|
106
|
+
}
|
|
107
|
+
/** Realpath an existing path. We always expect `workspaceRoot` to exist
|
|
108
|
+
* when invoked through the engine; if it doesn't we fall back to the
|
|
109
|
+
* original (lexical containment is then a no-op safety net). */
|
|
110
|
+
function realpathSafe(p) {
|
|
111
|
+
try {
|
|
112
|
+
return realpathSync(p);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return p;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Realpath a path that may or may not exist, resolving any symlinks
|
|
119
|
+
* at the leaf — including DANGLING symlinks (Round-2 MUST-FIX #3). A
|
|
120
|
+
* symlink whose target doesn't exist would otherwise be accepted by the
|
|
121
|
+
* lexical containment check; a subsequent `write` would then follow the
|
|
122
|
+
* symlink and create the target OUTSIDE the worktree. We catch this by
|
|
123
|
+
* `lstat`-ing the leaf first; if it's a symlink, we resolve the link
|
|
124
|
+
* target manually (relative-to-link-dir if not absolute) and recurse.
|
|
125
|
+
* Bounded by `SYMLINK_RESOLVE_MAX_DEPTH` to defeat link cycles.
|
|
126
|
+
*
|
|
127
|
+
* On depth exhaustion we **fail closed** (throw) rather than returning the
|
|
128
|
+
* unresolved path: returning the lexically-in-workspace symlink would let a
|
|
129
|
+
* `write` follow a >8-hop chain OUTSIDE the workspace — the very escape this
|
|
130
|
+
* resolution prevents (5.6-era symlink-hardening codex MUST-FIX). A legit
|
|
131
|
+
* path practically never chains >8 symlinks, so rejecting is the safe choice. */
|
|
132
|
+
const SYMLINK_RESOLVE_MAX_DEPTH = 8;
|
|
133
|
+
function realpathExtant(p, depth = 0) {
|
|
134
|
+
// Detect symlinks (live OR dangling) at the leaf BEFORE the
|
|
135
|
+
// `realpathSync` happy path — dangling symlinks make `realpathSync`
|
|
136
|
+
// throw ENOENT, which we previously swallowed and rejoined the leaf
|
|
137
|
+
// verbatim (the bug).
|
|
138
|
+
try {
|
|
139
|
+
const lst = lstatSync(p);
|
|
140
|
+
if (lst.isSymbolicLink()) {
|
|
141
|
+
if (depth >= SYMLINK_RESOLVE_MAX_DEPTH) {
|
|
142
|
+
throw new AppError(ErrorCode.Permission, `Symlink chain too deep (possible workspace escape): ${p}`);
|
|
143
|
+
}
|
|
144
|
+
const target = readlinkSync(p);
|
|
145
|
+
const resolved = isAbsolute(target) ? target : resolve(dirname(p), target);
|
|
146
|
+
return realpathExtant(resolved, depth + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
// Propagate our own fail-closed throw (and any deeper one from the
|
|
151
|
+
// recursion); only swallow the expected fs errors (lstat ENOENT on a
|
|
152
|
+
// non-existent leaf), which fall through to the ancestor walk below.
|
|
153
|
+
if (e instanceof AppError)
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return realpathSync(p);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
/* fall through */
|
|
161
|
+
}
|
|
162
|
+
let current = dirname(p);
|
|
163
|
+
let tail = p.slice(current.length);
|
|
164
|
+
while (current !== dirname(current)) {
|
|
165
|
+
try {
|
|
166
|
+
const real = realpathSync(current);
|
|
167
|
+
return join(real, tail);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
tail = current.slice(dirname(current).length) + tail;
|
|
171
|
+
current = dirname(current);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Reached the root without finding an existing ancestor; return the
|
|
175
|
+
// path lexically resolved (this is the standard cwd-relative form, so
|
|
176
|
+
// containment falls through to the lexical check above).
|
|
177
|
+
return p;
|
|
178
|
+
}
|
|
179
|
+
/** Render a string as a short JSON-quoted preview for permission prompts. */
|
|
180
|
+
export function preview(s) {
|
|
181
|
+
const one = s.replace(/\s+/g, " ").trim();
|
|
182
|
+
return JSON.stringify(one.length > 40 ? one.slice(0, 40) + "…" : one);
|
|
183
|
+
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// (5.1) Shared output-cleaning + persistence helpers used by `bash` and
|
|
186
|
+
// the new `pty` interactive tool. Both tools strip ANSI before showing
|
|
187
|
+
// the model the body, and persist oversized bodies to
|
|
188
|
+
// `.chances/tool-results/<id>/<file>` under restrictive POSIX perms.
|
|
189
|
+
//
|
|
190
|
+
// (5.7) The ANSI / control-char strippers moved to `@chances-ai/runtime`
|
|
191
|
+
// (`text-sanitize.ts`) so the bash tool, the pty registry, and the MCP /
|
|
192
|
+
// compaction / agents sanitizers all share ONE audited implementation
|
|
193
|
+
// instead of byte-copied regexes. Re-exported here so existing
|
|
194
|
+
// `./_shared.js` importers (bash, pty) are unchanged.
|
|
195
|
+
export { cleanOutput, normaliseLineEndings, stripAnsi } from "@chances-ai/runtime";
|
|
196
|
+
/** Map an engine call id to a filesystem-safe artifact-dir name (5.7 / codex
|
|
197
|
+
* R1 M2). A clean id (`toolu_…`, `call_…`) is used **verbatim** — that branch is
|
|
198
|
+
* injective and the dir name stays human-readable, joining 1:1 to the
|
|
199
|
+
* `tool_result` the client saw. Anything else — a hostile/unusual provider id
|
|
200
|
+
* containing `/`, `..`, or other bytes, OR an empty string — is replaced
|
|
201
|
+
* WHOLESALE by `id-<sha256[:16]>` rather than lossily mapped (`a/b` and `a_b`
|
|
202
|
+
* must NOT collide into one dir and overwrite each other). That hash branch is
|
|
203
|
+
* **collision-resistant, not strictly injective** (a 64-bit truncation), but
|
|
204
|
+
* for the volume of tool calls in a session the collision probability is
|
|
205
|
+
* negligible and a collision is never a security issue (worst case: two
|
|
206
|
+
* unrelated calls share a dir — never a traversal). Traversal is impossible in
|
|
207
|
+
* both branches: verbatim only admits `[A-Za-z0-9_-]`, the hash only emits hex. */
|
|
208
|
+
export function safeArtifactId(raw) {
|
|
209
|
+
if (/^[A-Za-z0-9_-]{1,128}$/.test(raw))
|
|
210
|
+
return raw;
|
|
211
|
+
return `id-${createHash("sha256").update(raw).digest("hex").slice(0, 16)}`;
|
|
212
|
+
}
|
|
213
|
+
/** Best-effort persistence under `<root>/.chances/tool-results/<id>/<file>`
|
|
214
|
+
* with restrictive POSIX permissions (0o700 dir, 0o600 file). Returns the
|
|
215
|
+
* absolute path on success, `null` on filesystem failure (the caller's
|
|
216
|
+
* banner still renders an inline preview either way, so persistence
|
|
217
|
+
* failure degrades to "model only sees the head"; not a fatal tool
|
|
218
|
+
* error).
|
|
219
|
+
*
|
|
220
|
+
* `id` is the engine's `ctx.callId` (5.7) when available — the artifact dir is
|
|
221
|
+
* then named for the real call so a client correlates the saved file with the
|
|
222
|
+
* `tool_result`. Falls back to a fresh `createId(prefix)` for non-engine
|
|
223
|
+
* callers (tests/plugins) — fully backward compatible.
|
|
224
|
+
*
|
|
225
|
+
* **Windows caveat.** Node's `fs` `mode` flag only affects the read-only
|
|
226
|
+
* bit on Windows; ACL-based access control is not enforced via this
|
|
227
|
+
* argument. POSIX-first audience; explicit DACL hardening is a follow-up.
|
|
228
|
+
*/
|
|
229
|
+
export function persistFullOutput(workspaceRoot, body, prefix = "tool", filename = "output.txt", id) {
|
|
230
|
+
try {
|
|
231
|
+
// `id !== undefined` (not truthiness) so an empty-string call id still
|
|
232
|
+
// routes through `safeArtifactId` (→ a hash dir) instead of silently
|
|
233
|
+
// falling back to a random `createId`, which would break correlation
|
|
234
|
+
// with `tool_result.callId === ""` (codex R2 Q2).
|
|
235
|
+
const dirName = id !== undefined ? safeArtifactId(id) : createId(prefix);
|
|
236
|
+
const dir = join(workspaceRoot, ".chances", "tool-results", dirName);
|
|
237
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
238
|
+
const path = join(dir, filename);
|
|
239
|
+
writeFileSync(path, body, { mode: 0o600 });
|
|
240
|
+
return path;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=_shared.js.map
|