apex-code 0.3.0 → 0.5.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/CHANGELOG.md +46 -0
- package/dist/client/transcript.d.ts.map +1 -1
- package/dist/client/transcript.js +1 -1
- package/dist/client/transcript.js.map +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +2 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +2 -2
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/durable-state/daemon.d.ts.map +1 -1
- package/dist/core/durable-state/daemon.js +1 -1
- package/dist/core/durable-state/daemon.js.map +1 -1
- package/dist/core/extensions/types.d.ts +1 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -1
- package/dist/core/mcp/mcp-tool.js +2 -4
- package/dist/core/mcp/mcp-tool.js.map +1 -1
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -1
- package/dist/core/mcp/oauth/authorize.js +7 -2
- package/dist/core/mcp/oauth/authorize.js.map +1 -1
- package/dist/core/mcp/runtime.d.ts +2 -2
- package/dist/core/mcp/runtime.d.ts.map +1 -1
- package/dist/core/mcp/runtime.js +8 -3
- package/dist/core/mcp/runtime.js.map +1 -1
- package/dist/core/permissions/store.d.ts +7 -2
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +6 -1
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/project-resources.d.ts +12 -0
- package/dist/core/project-resources.d.ts.map +1 -1
- package/dist/core/project-resources.js +19 -0
- package/dist/core/project-resources.js.map +1 -1
- package/dist/core/provider-composer.d.ts.map +1 -1
- package/dist/core/provider-composer.js +10 -1
- package/dist/core/provider-composer.js.map +1 -1
- package/dist/core/resource-loader.d.ts +6 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +9 -3
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +16 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +39 -16
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts +11 -7
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +37 -46
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +2 -2
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +3 -3
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +4 -4
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +4 -4
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +4 -4
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +9 -0
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +235 -10
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +11 -8
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +7 -8
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/workspace/state.d.ts.map +1 -1
- package/dist/core/workspace/state.js +1 -1
- package/dist/core/workspace/state.js.map +1 -1
- package/dist/extensions/llama/ui.d.ts.map +1 -1
- package/dist/extensions/llama/ui.js +3 -2
- package/dist/extensions/llama/ui.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +0 -10
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/autocomplete-source.d.ts +8 -0
- package/dist/modes/interactive/components/autocomplete-source.d.ts.map +1 -0
- package/dist/modes/interactive/components/autocomplete-source.js +23 -0
- package/dist/modes/interactive/components/autocomplete-source.js.map +1 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +6 -4
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.js +1 -1
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/dist/modes/interactive/components/composer-dock.d.ts +12 -0
- package/dist/modes/interactive/components/composer-dock.d.ts.map +1 -0
- package/dist/modes/interactive/components/composer-dock.js +24 -0
- package/dist/modes/interactive/components/composer-dock.js.map +1 -0
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +11 -9
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/dist/modes/interactive/components/diff.js +2 -2
- package/dist/modes/interactive/components/diff.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +9 -9
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-input.js +9 -5
- package/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +11 -10
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +6 -6
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +8 -3
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +21 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +34 -0
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +27 -23
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +14 -18
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +7 -6
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -1
- package/dist/modes/interactive/components/permission-preview.js +3 -3
- package/dist/modes/interactive/components/permission-preview.js.map +1 -1
- package/dist/modes/interactive/components/prompt-input.d.ts +12 -0
- package/dist/modes/interactive/components/prompt-input.d.ts.map +1 -0
- package/dist/modes/interactive/components/prompt-input.js +18 -0
- package/dist/modes/interactive/components/prompt-input.js.map +1 -0
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +4 -3
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +26 -19
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +39 -6
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.js +8 -6
- package/dist/modes/interactive/components/settings-submenu.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +10 -5
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +1 -11
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +9 -38
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-panel.js +7 -2
- package/dist/modes/interactive/components/tool-panel.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +9 -5
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/trust-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/trust-selector.js +11 -11
- package/dist/modes/interactive/components/trust-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +2 -2
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +6 -3
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +7 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +65 -65
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +49 -23
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/tsconfig.json +3 -0
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +1 -1
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +22 -22
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/docs/json.md +36 -0
- package/docs/models.md +1 -1
- package/docs/settings.md +1 -1
- package/examples/extensions/gondolin/package-lock.json +3 -3
- package/examples/extensions/minimal-mode.ts +6 -6
- package/examples/extensions/sandbox/package-lock.json +4 -4
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/sdk/10-settings.ts +2 -2
- package/examples/sdk/README.md +1 -1
- package/npm-shrinkwrap.json +19 -34
- package/package.json +6 -6
- package/dist/modes/interactive/components/message-spine.d.ts +0 -14
- package/dist/modes/interactive/components/message-spine.d.ts.map +0 -1
- package/dist/modes/interactive/components/message-spine.js +0 -19
- package/dist/modes/interactive/components/message-spine.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-tool.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/mcp-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,KAAK,cAAc,EAAsB,aAAa,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9D,eAAO,MAAM,aAAa,QAAQ,CAAC;AAwCnC,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC9C,KAAK,EAAE,gBAAgB,CAAC;IACxB,OAAO,EAAE,gBAAgB,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,CACtC,OAAO,EAAE,cAAc,GACrB,kBAAkB,CAAC,OAAO,aAAa,EAAE,cAAc,CAAC,CA0I1D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc;;;;;;oBAEpD","sourcesContent":["/**\n * One tool for every MCP server, instead of one tool per server tool.\n *\n * Registering each server tool individually costs 150-300 prompt tokens apiece, and\n * two ordinary servers would exceed the whole static-prefix budget before a\n * conversation starts. The proxy costs one announced name and this description,\n * because its schema defers.\n *\n * `search` and `describe` read the disk cache and reach no server. Only a `tool` call\n * connects, which is what makes discovery free and is why the two have separate\n * permission rules (ADR 0025).\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport type { ApexToolDefinition } from \"../tools/contract.ts\";\nimport { wrapToolDefinition } from \"../tools/tool-definition-wrapper.ts\";\nimport { createMcpToolContract } from \"./contract.ts\";\nimport type { McpMetadataCache } from \"./metadata-cache.ts\";\nimport { McpOAuthRefreshError, McpOAuthRequiredError } from \"./oauth/mcp-token.ts\";\nimport { type McpToolDetails, type McpToolParams, mcpToolSchema } from \"./schema.ts\";\nimport type { McpServerManager } from \"./server-manager.ts\";\nimport type { CachedTool, McpServerConfig } from \"./types.ts\";\n\nexport const MCP_TOOL_NAME = \"mcp\";\n\nconst MAX_RESULTS = 25;\n\nconst DESCRIPTION =\n\t\"Use tools provided by configured MCP servers. \" +\n\t'Search with {\"search\":\"keyword\"}, list one server with {\"server\":\"name\"}, ' +\n\t'see a tool\\'s full schema with {\"describe\":\"server:tool\"}, ' +\n\t'and call one with {\"tool\":\"server:tool\",\"args\":{...}}. ' +\n\t\"Search and describe read a local cache and never start a server, so discovery is free. \" +\n\t\"Always describe a tool before calling it for the first time.\";\n\nfunction text(body: string): AgentToolResult<McpToolDetails>[\"content\"] {\n\treturn [{ type: \"text\", text: body }];\n}\n\nfunction qualified(tool: CachedTool): string {\n\treturn `${tool.server}:${tool.name}`;\n}\n\n/** Substring scoring, weighted to the name, which is what the model searched for. */\nfunction score(tool: CachedTool, needle: string): number {\n\tconst name = qualified(tool).toLowerCase();\n\tconst description = tool.description.toLowerCase();\n\tif (name === needle) return 100;\n\tif (name.includes(needle)) return 50;\n\tif (description.includes(needle)) return 10;\n\treturn 0;\n}\n\nfunction renderList(tools: readonly CachedTool[]): string {\n\treturn tools.map((tool) => `${qualified(tool)}: ${tool.description}`).join(\"\\n\");\n}\n\nfunction nearest(tools: readonly CachedTool[], target: string): CachedTool[] {\n\tconst needle = target.toLowerCase();\n\tconst stem = needle.slice(0, Math.max(3, Math.floor(needle.length * 0.6)));\n\treturn tools.filter((tool) => qualified(tool).toLowerCase().includes(stem)).slice(0, 5);\n}\n\nexport interface McpToolOptions {\n\tservers: ReadonlyMap<string, McpServerConfig>;\n\tcache: McpMetadataCache;\n\tmanager: McpServerManager;\n}\n\nexport function createMcpToolDefinition(\n\toptions: McpToolOptions,\n): ApexToolDefinition<typeof mcpToolSchema, McpToolDetails> {\n\tconst { servers, cache, manager } = options;\n\n\treturn {\n\t\tname: MCP_TOOL_NAME,\n\t\tlabel: \"mcp\",\n\t\tdescription: DESCRIPTION,\n\t\tpromptSnippet: \"Call tools on configured MCP servers\",\n\t\tparameters: mcpToolSchema,\n\t\tcontract: createMcpToolContract(servers),\n\t\tasync execute(_id, params: McpToolParams): Promise<AgentToolResult<McpToolDetails>> {\n\t\t\tif (params.tool) return callTool(params.tool, params.args);\n\t\t\tif (params.describe) return describeTool(params.describe);\n\t\t\tif (params.search !== undefined) return searchTools(params.search, params.server);\n\t\t\tif (params.server) return listServer(params.server);\n\n\t\t\
|
|
1
|
+
{"version":3,"file":"mcp-tool.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/mcp-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,KAAK,cAAc,EAAsB,aAAa,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9D,eAAO,MAAM,aAAa,QAAQ,CAAC;AAwCnC,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC9C,KAAK,EAAE,gBAAgB,CAAC;IACxB,OAAO,EAAE,gBAAgB,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,CACtC,OAAO,EAAE,cAAc,GACrB,kBAAkB,CAAC,OAAO,aAAa,EAAE,cAAc,CAAC,CA0I1D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc;;;;;;oBAEpD","sourcesContent":["/**\n * One tool for every MCP server, instead of one tool per server tool.\n *\n * Registering each server tool individually costs 150-300 prompt tokens apiece, and\n * two ordinary servers would exceed the whole static-prefix budget before a\n * conversation starts. The proxy costs one announced name and this description,\n * because its schema defers.\n *\n * `search` and `describe` read the disk cache and reach no server. Only a `tool` call\n * connects, which is what makes discovery free and is why the two have separate\n * permission rules (ADR 0025).\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport type { ApexToolDefinition } from \"../tools/contract.ts\";\nimport { wrapToolDefinition } from \"../tools/tool-definition-wrapper.ts\";\nimport { createMcpToolContract } from \"./contract.ts\";\nimport type { McpMetadataCache } from \"./metadata-cache.ts\";\nimport { McpOAuthRefreshError, McpOAuthRequiredError } from \"./oauth/mcp-token.ts\";\nimport { type McpToolDetails, type McpToolParams, mcpToolSchema } from \"./schema.ts\";\nimport type { McpServerManager } from \"./server-manager.ts\";\nimport type { CachedTool, McpServerConfig } from \"./types.ts\";\n\nexport const MCP_TOOL_NAME = \"mcp\";\n\nconst MAX_RESULTS = 25;\n\nconst DESCRIPTION =\n\t\"Use tools provided by configured MCP servers. \" +\n\t'Search with {\"search\":\"keyword\"}, list one server with {\"server\":\"name\"}, ' +\n\t'see a tool\\'s full schema with {\"describe\":\"server:tool\"}, ' +\n\t'and call one with {\"tool\":\"server:tool\",\"args\":{...}}. ' +\n\t\"Search and describe read a local cache and never start a server, so discovery is free. \" +\n\t\"Always describe a tool before calling it for the first time.\";\n\nfunction text(body: string): AgentToolResult<McpToolDetails>[\"content\"] {\n\treturn [{ type: \"text\", text: body }];\n}\n\nfunction qualified(tool: CachedTool): string {\n\treturn `${tool.server}:${tool.name}`;\n}\n\n/** Substring scoring, weighted to the name, which is what the model searched for. */\nfunction score(tool: CachedTool, needle: string): number {\n\tconst name = qualified(tool).toLowerCase();\n\tconst description = tool.description.toLowerCase();\n\tif (name === needle) return 100;\n\tif (name.includes(needle)) return 50;\n\tif (description.includes(needle)) return 10;\n\treturn 0;\n}\n\nfunction renderList(tools: readonly CachedTool[]): string {\n\treturn tools.map((tool) => `${qualified(tool)}: ${tool.description}`).join(\"\\n\");\n}\n\nfunction nearest(tools: readonly CachedTool[], target: string): CachedTool[] {\n\tconst needle = target.toLowerCase();\n\tconst stem = needle.slice(0, Math.max(3, Math.floor(needle.length * 0.6)));\n\treturn tools.filter((tool) => qualified(tool).toLowerCase().includes(stem)).slice(0, 5);\n}\n\nexport interface McpToolOptions {\n\tservers: ReadonlyMap<string, McpServerConfig>;\n\tcache: McpMetadataCache;\n\tmanager: McpServerManager;\n}\n\nexport function createMcpToolDefinition(\n\toptions: McpToolOptions,\n): ApexToolDefinition<typeof mcpToolSchema, McpToolDetails> {\n\tconst { servers, cache, manager } = options;\n\n\treturn {\n\t\tname: MCP_TOOL_NAME,\n\t\tlabel: \"mcp\",\n\t\tdescription: DESCRIPTION,\n\t\tpromptSnippet: \"Call tools on configured MCP servers\",\n\t\tparameters: mcpToolSchema,\n\t\tcontract: createMcpToolContract(servers),\n\t\tasync execute(_id, params: McpToolParams): Promise<AgentToolResult<McpToolDetails>> {\n\t\t\tif (params.tool) return callTool(params.tool, params.args);\n\t\t\tif (params.describe) return describeTool(params.describe);\n\t\t\tif (params.search !== undefined) return searchTools(params.search, params.server);\n\t\t\tif (params.server) return listServer(params.server);\n\n\t\t\tthrow new Error(\n\t\t\t\t'Provide one of \"search\", \"server\", \"describe\", or \"tool\". ' +\n\t\t\t\t\t'If the parameters are unavailable, call tool_schema with {\"name\":\"mcp\"} first, then retry with an operation such as {\"search\":\"keyword\"}.',\n\t\t\t);\n\t\t},\n\t};\n\n\tfunction searchTools(needle: string, serverFilter?: string): AgentToolResult<McpToolDetails> {\n\t\tconst pool = serverFilter ? cache.all().filter((tool) => tool.server === serverFilter) : cache.all();\n\t\tconst lowered = needle.toLowerCase();\n\t\tconst ranked = pool\n\t\t\t.map((tool) => ({ tool, rank: score(tool, lowered) }))\n\t\t\t.filter((entry) => entry.rank > 0)\n\t\t\t.sort((a, b) => b.rank - a.rank)\n\t\t\t.slice(0, MAX_RESULTS)\n\t\t\t.map((entry) => entry.tool);\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\tranked.length > 0\n\t\t\t\t\t? renderList(ranked)\n\t\t\t\t\t: `No cached MCP tool matches \"${needle}\". Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t),\n\t\t\tdetails: { action: \"search\", server: serverFilter, tool: undefined },\n\t\t};\n\t}\n\n\tfunction listServer(name: string): AgentToolResult<McpToolDetails> {\n\t\tif (!servers.has(name)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${name}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t\t};\n\t\t}\n\n\t\tconst tools = cache.all().filter((tool) => tool.server === name);\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\ttools.length > 0\n\t\t\t\t\t? renderList(tools)\n\t\t\t\t\t: `No cached tools for \"${name}\" yet. Tools are cached the first time the server is called, so call a known tool on it, or set \"lifecycle\": \"eager\" for this server in .mcp.json to have them cached at startup.`,\n\t\t\t),\n\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t};\n\t}\n\n\tfunction describeTool(target: string): AgentToolResult<McpToolDetails> {\n\t\tconst found = cache.all().find((tool) => qualified(tool) === target);\n\t\tif (!found) {\n\t\t\tconst suggestions = nearest(cache.all(), target);\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\tsuggestions.length > 0\n\t\t\t\t\t\t? `No cached tool named \"${target}\". Did you mean:\\n${renderList(suggestions)}`\n\t\t\t\t\t\t: `No cached tool named \"${target}\". Search with {\"search\":\"keyword\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"describe\", server: undefined, tool: target },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\t`${qualified(found)}\\n${found.description}\\n\\nInput schema:\\n${JSON.stringify(found.inputSchema, null, 2)}`,\n\t\t\t),\n\t\t\tdetails: { action: \"describe\", server: found.server, tool: target },\n\t\t};\n\t}\n\n\tasync function callTool(target: string, args: unknown): Promise<AgentToolResult<McpToolDetails>> {\n\t\tconst separator = target.indexOf(\":\");\n\t\tconst details: McpToolDetails = { action: \"call\", server: undefined, tool: target };\n\t\tif (separator <= 0) {\n\t\t\treturn { content: text(`Tool must be qualified as \"server:tool\". Received \"${target}\".`), details };\n\t\t}\n\n\t\tconst serverName = target.slice(0, separator);\n\t\tconst toolName = target.slice(separator + 1);\n\t\tdetails.server = serverName;\n\n\t\tif (!servers.has(serverName)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await manager.withConnection(serverName, async (connection) => {\n\t\t\t\t// An empty list is a fact about the server, not a failed read. Keeping the\n\t\t\t\t// previous entry would leave search advertising tools that no longer exist.\n\t\t\t\tconst fresh = await connection.listTools();\n\t\t\t\tcache.set(servers.get(serverName) as McpServerConfig, fresh);\n\t\t\t\tcache.save();\n\t\t\t\treturn connection.callTool(toolName, args);\n\t\t\t});\n\n\t\t\t// `AgentToolResult` has no error flag; a server-reported failure is marked in\n\t\t\t// the text so the model can see it failed without the turn being aborted.\n\t\t\tconst content = result.isError\n\t\t\t\t? text(`Tool reported an error:\\n${result.content.map((part) => part.text).join(\"\\n\")}`)\n\t\t\t\t: result.content;\n\t\t\treturn { content, details };\n\t\t} catch (error) {\n\t\t\t// OAuth failures already carry the one action that helps (`apex-code mcp auth\n\t\t\t// <server>`); the generic \"unavailable\" wrapper would bury it.\n\t\t\tif (error instanceof McpOAuthRequiredError || error instanceof McpOAuthRefreshError) {\n\t\t\t\treturn { content: text(error.message), details };\n\t\t\t}\n\t\t\t// A failed server is reported, not retried: the manager holds it in `failed`\n\t\t\t// with a backoff window so one broken server cannot cost every call a spawn.\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is unavailable: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\t}\n}\n\nexport function createMcpTool(options: McpToolOptions) {\n\treturn wrapToolDefinition(createMcpToolDefinition(options));\n}\n"]}
|
|
@@ -66,10 +66,8 @@ export function createMcpToolDefinition(options) {
|
|
|
66
66
|
return searchTools(params.search, params.server);
|
|
67
67
|
if (params.server)
|
|
68
68
|
return listServer(params.server);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
details: { action: "search", server: undefined, tool: undefined },
|
|
72
|
-
};
|
|
69
|
+
throw new Error('Provide one of "search", "server", "describe", or "tool". ' +
|
|
70
|
+
'If the parameters are unavailable, call tool_schema with {"name":"mcp"} first, then retry with an operation such as {"search":"keyword"}.');
|
|
73
71
|
},
|
|
74
72
|
};
|
|
75
73
|
function searchTools(needle, serverFilter) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-tool.js","sourceRoot":"","sources":["../../../src/core/mcp/mcp-tool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One tool for every MCP server, instead of one tool per server tool.\n *\n * Registering each server tool individually costs 150-300 prompt tokens apiece, and\n * two ordinary servers would exceed the whole static-prefix budget before a\n * conversation starts. The proxy costs one announced name and this description,\n * because its schema defers.\n *\n * `search` and `describe` read the disk cache and reach no server. Only a `tool` call\n * connects, which is what makes discovery free and is why the two have separate\n * permission rules (ADR 0025).\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport type { ApexToolDefinition } from \"../tools/contract.ts\";\nimport { wrapToolDefinition } from \"../tools/tool-definition-wrapper.ts\";\nimport { createMcpToolContract } from \"./contract.ts\";\nimport type { McpMetadataCache } from \"./metadata-cache.ts\";\nimport { McpOAuthRefreshError, McpOAuthRequiredError } from \"./oauth/mcp-token.ts\";\nimport { type McpToolDetails, type McpToolParams, mcpToolSchema } from \"./schema.ts\";\nimport type { McpServerManager } from \"./server-manager.ts\";\nimport type { CachedTool, McpServerConfig } from \"./types.ts\";\n\nexport const MCP_TOOL_NAME = \"mcp\";\n\nconst MAX_RESULTS = 25;\n\nconst DESCRIPTION =\n\t\"Use tools provided by configured MCP servers. \" +\n\t'Search with {\"search\":\"keyword\"}, list one server with {\"server\":\"name\"}, ' +\n\t'see a tool\\'s full schema with {\"describe\":\"server:tool\"}, ' +\n\t'and call one with {\"tool\":\"server:tool\",\"args\":{...}}. ' +\n\t\"Search and describe read a local cache and never start a server, so discovery is free. \" +\n\t\"Always describe a tool before calling it for the first time.\";\n\nfunction text(body: string): AgentToolResult<McpToolDetails>[\"content\"] {\n\treturn [{ type: \"text\", text: body }];\n}\n\nfunction qualified(tool: CachedTool): string {\n\treturn `${tool.server}:${tool.name}`;\n}\n\n/** Substring scoring, weighted to the name, which is what the model searched for. */\nfunction score(tool: CachedTool, needle: string): number {\n\tconst name = qualified(tool).toLowerCase();\n\tconst description = tool.description.toLowerCase();\n\tif (name === needle) return 100;\n\tif (name.includes(needle)) return 50;\n\tif (description.includes(needle)) return 10;\n\treturn 0;\n}\n\nfunction renderList(tools: readonly CachedTool[]): string {\n\treturn tools.map((tool) => `${qualified(tool)}: ${tool.description}`).join(\"\\n\");\n}\n\nfunction nearest(tools: readonly CachedTool[], target: string): CachedTool[] {\n\tconst needle = target.toLowerCase();\n\tconst stem = needle.slice(0, Math.max(3, Math.floor(needle.length * 0.6)));\n\treturn tools.filter((tool) => qualified(tool).toLowerCase().includes(stem)).slice(0, 5);\n}\n\nexport interface McpToolOptions {\n\tservers: ReadonlyMap<string, McpServerConfig>;\n\tcache: McpMetadataCache;\n\tmanager: McpServerManager;\n}\n\nexport function createMcpToolDefinition(\n\toptions: McpToolOptions,\n): ApexToolDefinition<typeof mcpToolSchema, McpToolDetails> {\n\tconst { servers, cache, manager } = options;\n\n\treturn {\n\t\tname: MCP_TOOL_NAME,\n\t\tlabel: \"mcp\",\n\t\tdescription: DESCRIPTION,\n\t\tpromptSnippet: \"Call tools on configured MCP servers\",\n\t\tparameters: mcpToolSchema,\n\t\tcontract: createMcpToolContract(servers),\n\t\tasync execute(_id, params: McpToolParams): Promise<AgentToolResult<McpToolDetails>> {\n\t\t\tif (params.tool) return callTool(params.tool, params.args);\n\t\t\tif (params.describe) return describeTool(params.describe);\n\t\t\tif (params.search !== undefined) return searchTools(params.search, params.server);\n\t\t\tif (params.server) return listServer(params.server);\n\n\t\t\treturn {\n\t\t\t\tcontent: text('Provide one of \"search\", \"server\", \"describe\", or \"tool\".'),\n\t\t\t\tdetails: { action: \"search\", server: undefined, tool: undefined },\n\t\t\t};\n\t\t},\n\t};\n\n\tfunction searchTools(needle: string, serverFilter?: string): AgentToolResult<McpToolDetails> {\n\t\tconst pool = serverFilter ? cache.all().filter((tool) => tool.server === serverFilter) : cache.all();\n\t\tconst lowered = needle.toLowerCase();\n\t\tconst ranked = pool\n\t\t\t.map((tool) => ({ tool, rank: score(tool, lowered) }))\n\t\t\t.filter((entry) => entry.rank > 0)\n\t\t\t.sort((a, b) => b.rank - a.rank)\n\t\t\t.slice(0, MAX_RESULTS)\n\t\t\t.map((entry) => entry.tool);\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\tranked.length > 0\n\t\t\t\t\t? renderList(ranked)\n\t\t\t\t\t: `No cached MCP tool matches \"${needle}\". Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t),\n\t\t\tdetails: { action: \"search\", server: serverFilter, tool: undefined },\n\t\t};\n\t}\n\n\tfunction listServer(name: string): AgentToolResult<McpToolDetails> {\n\t\tif (!servers.has(name)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${name}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t\t};\n\t\t}\n\n\t\tconst tools = cache.all().filter((tool) => tool.server === name);\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\ttools.length > 0\n\t\t\t\t\t? renderList(tools)\n\t\t\t\t\t: `No cached tools for \"${name}\" yet. Tools are cached the first time the server is called, so call a known tool on it, or set \"lifecycle\": \"eager\" for this server in .mcp.json to have them cached at startup.`,\n\t\t\t),\n\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t};\n\t}\n\n\tfunction describeTool(target: string): AgentToolResult<McpToolDetails> {\n\t\tconst found = cache.all().find((tool) => qualified(tool) === target);\n\t\tif (!found) {\n\t\t\tconst suggestions = nearest(cache.all(), target);\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\tsuggestions.length > 0\n\t\t\t\t\t\t? `No cached tool named \"${target}\". Did you mean:\\n${renderList(suggestions)}`\n\t\t\t\t\t\t: `No cached tool named \"${target}\". Search with {\"search\":\"keyword\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"describe\", server: undefined, tool: target },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\t`${qualified(found)}\\n${found.description}\\n\\nInput schema:\\n${JSON.stringify(found.inputSchema, null, 2)}`,\n\t\t\t),\n\t\t\tdetails: { action: \"describe\", server: found.server, tool: target },\n\t\t};\n\t}\n\n\tasync function callTool(target: string, args: unknown): Promise<AgentToolResult<McpToolDetails>> {\n\t\tconst separator = target.indexOf(\":\");\n\t\tconst details: McpToolDetails = { action: \"call\", server: undefined, tool: target };\n\t\tif (separator <= 0) {\n\t\t\treturn { content: text(`Tool must be qualified as \"server:tool\". Received \"${target}\".`), details };\n\t\t}\n\n\t\tconst serverName = target.slice(0, separator);\n\t\tconst toolName = target.slice(separator + 1);\n\t\tdetails.server = serverName;\n\n\t\tif (!servers.has(serverName)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await manager.withConnection(serverName, async (connection) => {\n\t\t\t\t// An empty list is a fact about the server, not a failed read. Keeping the\n\t\t\t\t// previous entry would leave search advertising tools that no longer exist.\n\t\t\t\tconst fresh = await connection.listTools();\n\t\t\t\tcache.set(servers.get(serverName) as McpServerConfig, fresh);\n\t\t\t\tcache.save();\n\t\t\t\treturn connection.callTool(toolName, args);\n\t\t\t});\n\n\t\t\t// `AgentToolResult` has no error flag; a server-reported failure is marked in\n\t\t\t// the text so the model can see it failed without the turn being aborted.\n\t\t\tconst content = result.isError\n\t\t\t\t? text(`Tool reported an error:\\n${result.content.map((part) => part.text).join(\"\\n\")}`)\n\t\t\t\t: result.content;\n\t\t\treturn { content, details };\n\t\t} catch (error) {\n\t\t\t// OAuth failures already carry the one action that helps (`apex-code mcp auth\n\t\t\t// <server>`); the generic \"unavailable\" wrapper would bury it.\n\t\t\tif (error instanceof McpOAuthRequiredError || error instanceof McpOAuthRefreshError) {\n\t\t\t\treturn { content: text(error.message), details };\n\t\t\t}\n\t\t\t// A failed server is reported, not retried: the manager holds it in `failed`\n\t\t\t// with a backoff window so one broken server cannot cost every call a spawn.\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is unavailable: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\t}\n}\n\nexport function createMcpTool(options: McpToolOptions) {\n\treturn wrapToolDefinition(createMcpToolDefinition(options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"mcp-tool.js","sourceRoot":"","sources":["../../../src/core/mcp/mcp-tool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One tool for every MCP server, instead of one tool per server tool.\n *\n * Registering each server tool individually costs 150-300 prompt tokens apiece, and\n * two ordinary servers would exceed the whole static-prefix budget before a\n * conversation starts. The proxy costs one announced name and this description,\n * because its schema defers.\n *\n * `search` and `describe` read the disk cache and reach no server. Only a `tool` call\n * connects, which is what makes discovery free and is why the two have separate\n * permission rules (ADR 0025).\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport type { ApexToolDefinition } from \"../tools/contract.ts\";\nimport { wrapToolDefinition } from \"../tools/tool-definition-wrapper.ts\";\nimport { createMcpToolContract } from \"./contract.ts\";\nimport type { McpMetadataCache } from \"./metadata-cache.ts\";\nimport { McpOAuthRefreshError, McpOAuthRequiredError } from \"./oauth/mcp-token.ts\";\nimport { type McpToolDetails, type McpToolParams, mcpToolSchema } from \"./schema.ts\";\nimport type { McpServerManager } from \"./server-manager.ts\";\nimport type { CachedTool, McpServerConfig } from \"./types.ts\";\n\nexport const MCP_TOOL_NAME = \"mcp\";\n\nconst MAX_RESULTS = 25;\n\nconst DESCRIPTION =\n\t\"Use tools provided by configured MCP servers. \" +\n\t'Search with {\"search\":\"keyword\"}, list one server with {\"server\":\"name\"}, ' +\n\t'see a tool\\'s full schema with {\"describe\":\"server:tool\"}, ' +\n\t'and call one with {\"tool\":\"server:tool\",\"args\":{...}}. ' +\n\t\"Search and describe read a local cache and never start a server, so discovery is free. \" +\n\t\"Always describe a tool before calling it for the first time.\";\n\nfunction text(body: string): AgentToolResult<McpToolDetails>[\"content\"] {\n\treturn [{ type: \"text\", text: body }];\n}\n\nfunction qualified(tool: CachedTool): string {\n\treturn `${tool.server}:${tool.name}`;\n}\n\n/** Substring scoring, weighted to the name, which is what the model searched for. */\nfunction score(tool: CachedTool, needle: string): number {\n\tconst name = qualified(tool).toLowerCase();\n\tconst description = tool.description.toLowerCase();\n\tif (name === needle) return 100;\n\tif (name.includes(needle)) return 50;\n\tif (description.includes(needle)) return 10;\n\treturn 0;\n}\n\nfunction renderList(tools: readonly CachedTool[]): string {\n\treturn tools.map((tool) => `${qualified(tool)}: ${tool.description}`).join(\"\\n\");\n}\n\nfunction nearest(tools: readonly CachedTool[], target: string): CachedTool[] {\n\tconst needle = target.toLowerCase();\n\tconst stem = needle.slice(0, Math.max(3, Math.floor(needle.length * 0.6)));\n\treturn tools.filter((tool) => qualified(tool).toLowerCase().includes(stem)).slice(0, 5);\n}\n\nexport interface McpToolOptions {\n\tservers: ReadonlyMap<string, McpServerConfig>;\n\tcache: McpMetadataCache;\n\tmanager: McpServerManager;\n}\n\nexport function createMcpToolDefinition(\n\toptions: McpToolOptions,\n): ApexToolDefinition<typeof mcpToolSchema, McpToolDetails> {\n\tconst { servers, cache, manager } = options;\n\n\treturn {\n\t\tname: MCP_TOOL_NAME,\n\t\tlabel: \"mcp\",\n\t\tdescription: DESCRIPTION,\n\t\tpromptSnippet: \"Call tools on configured MCP servers\",\n\t\tparameters: mcpToolSchema,\n\t\tcontract: createMcpToolContract(servers),\n\t\tasync execute(_id, params: McpToolParams): Promise<AgentToolResult<McpToolDetails>> {\n\t\t\tif (params.tool) return callTool(params.tool, params.args);\n\t\t\tif (params.describe) return describeTool(params.describe);\n\t\t\tif (params.search !== undefined) return searchTools(params.search, params.server);\n\t\t\tif (params.server) return listServer(params.server);\n\n\t\t\tthrow new Error(\n\t\t\t\t'Provide one of \"search\", \"server\", \"describe\", or \"tool\". ' +\n\t\t\t\t\t'If the parameters are unavailable, call tool_schema with {\"name\":\"mcp\"} first, then retry with an operation such as {\"search\":\"keyword\"}.',\n\t\t\t);\n\t\t},\n\t};\n\n\tfunction searchTools(needle: string, serverFilter?: string): AgentToolResult<McpToolDetails> {\n\t\tconst pool = serverFilter ? cache.all().filter((tool) => tool.server === serverFilter) : cache.all();\n\t\tconst lowered = needle.toLowerCase();\n\t\tconst ranked = pool\n\t\t\t.map((tool) => ({ tool, rank: score(tool, lowered) }))\n\t\t\t.filter((entry) => entry.rank > 0)\n\t\t\t.sort((a, b) => b.rank - a.rank)\n\t\t\t.slice(0, MAX_RESULTS)\n\t\t\t.map((entry) => entry.tool);\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\tranked.length > 0\n\t\t\t\t\t? renderList(ranked)\n\t\t\t\t\t: `No cached MCP tool matches \"${needle}\". Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t),\n\t\t\tdetails: { action: \"search\", server: serverFilter, tool: undefined },\n\t\t};\n\t}\n\n\tfunction listServer(name: string): AgentToolResult<McpToolDetails> {\n\t\tif (!servers.has(name)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${name}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t\t};\n\t\t}\n\n\t\tconst tools = cache.all().filter((tool) => tool.server === name);\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\ttools.length > 0\n\t\t\t\t\t? renderList(tools)\n\t\t\t\t\t: `No cached tools for \"${name}\" yet. Tools are cached the first time the server is called, so call a known tool on it, or set \"lifecycle\": \"eager\" for this server in .mcp.json to have them cached at startup.`,\n\t\t\t),\n\t\t\tdetails: { action: \"list\", server: name, tool: undefined },\n\t\t};\n\t}\n\n\tfunction describeTool(target: string): AgentToolResult<McpToolDetails> {\n\t\tconst found = cache.all().find((tool) => qualified(tool) === target);\n\t\tif (!found) {\n\t\t\tconst suggestions = nearest(cache.all(), target);\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\tsuggestions.length > 0\n\t\t\t\t\t\t? `No cached tool named \"${target}\". Did you mean:\\n${renderList(suggestions)}`\n\t\t\t\t\t\t: `No cached tool named \"${target}\". Search with {\"search\":\"keyword\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails: { action: \"describe\", server: undefined, tool: target },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: text(\n\t\t\t\t`${qualified(found)}\\n${found.description}\\n\\nInput schema:\\n${JSON.stringify(found.inputSchema, null, 2)}`,\n\t\t\t),\n\t\t\tdetails: { action: \"describe\", server: found.server, tool: target },\n\t\t};\n\t}\n\n\tasync function callTool(target: string, args: unknown): Promise<AgentToolResult<McpToolDetails>> {\n\t\tconst separator = target.indexOf(\":\");\n\t\tconst details: McpToolDetails = { action: \"call\", server: undefined, tool: target };\n\t\tif (separator <= 0) {\n\t\t\treturn { content: text(`Tool must be qualified as \"server:tool\". Received \"${target}\".`), details };\n\t\t}\n\n\t\tconst serverName = target.slice(0, separator);\n\t\tconst toolName = target.slice(separator + 1);\n\t\tdetails.server = serverName;\n\n\t\tif (!servers.has(serverName)) {\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is not configured. Configured servers: ${[...servers.keys()].join(\", \") || \"none\"}.`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await manager.withConnection(serverName, async (connection) => {\n\t\t\t\t// An empty list is a fact about the server, not a failed read. Keeping the\n\t\t\t\t// previous entry would leave search advertising tools that no longer exist.\n\t\t\t\tconst fresh = await connection.listTools();\n\t\t\t\tcache.set(servers.get(serverName) as McpServerConfig, fresh);\n\t\t\t\tcache.save();\n\t\t\t\treturn connection.callTool(toolName, args);\n\t\t\t});\n\n\t\t\t// `AgentToolResult` has no error flag; a server-reported failure is marked in\n\t\t\t// the text so the model can see it failed without the turn being aborted.\n\t\t\tconst content = result.isError\n\t\t\t\t? text(`Tool reported an error:\\n${result.content.map((part) => part.text).join(\"\\n\")}`)\n\t\t\t\t: result.content;\n\t\t\treturn { content, details };\n\t\t} catch (error) {\n\t\t\t// OAuth failures already carry the one action that helps (`apex-code mcp auth\n\t\t\t// <server>`); the generic \"unavailable\" wrapper would bury it.\n\t\t\tif (error instanceof McpOAuthRequiredError || error instanceof McpOAuthRefreshError) {\n\t\t\t\treturn { content: text(error.message), details };\n\t\t\t}\n\t\t\t// A failed server is reported, not retried: the manager holds it in `failed`\n\t\t\t// with a backoff window so one broken server cannot cost every call a spawn.\n\t\t\treturn {\n\t\t\t\tcontent: text(\n\t\t\t\t\t`MCP server \"${serverName}\" is unavailable: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t),\n\t\t\t\tdetails,\n\t\t\t};\n\t\t}\n\t}\n}\n\nexport function createMcpTool(options: McpToolOptions) {\n\treturn wrapToolDefinition(createMcpToolDefinition(options));\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,WAAW,sBAAsB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,kCAAkC;IAClC,IAAI,CAAC,EAAE,OAAO,eAAe,CAAC;IAC9B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B;AAED,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkC9F","sourcesContent":["/**\n * Shared entry point for `apex-code mcp auth <server>` and the TUI `/mcp auth`\n * command. Resolves the named server from the merged MCP config, checks it is an\n * OAuth-configured HTTP server, and runs the flow. UI-agnostic: callers supply the\n * line printer and the browser opener.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { projectResourcePathByName } from \"../../project-resources.ts\";\nimport { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from \"../config.ts\";\nimport { runMcpOAuthFlow } from \"./flow.ts\";\n\nexport interface AuthorizeServerOptions {\n\tserverName: string;\n\t/** Project root for `.mcp.json` discovery. Default: process.cwd(). */\n\tcwd?: string;\n\t/** Agent directory the user-scope `mcp.json` is resolved against. Default: the host's. */\n\tagentDir?: string;\n\t/** Credential sink. Default: the host store (`AuthStorage.create()`). */\n\tcredentials?: CredentialStore;\n\topenBrowser?: (url: string) => void;\n\t/** Injectable flow, for tests. */\n\tflow?: typeof runMcpOAuthFlow;\n\tlog: (line: string) => void;\n}\n\nexport async function authorizeConfiguredServer(options: AuthorizeServerOptions): Promise<void> {\n\tconst { servers } = loadMcpConfig({\n\t\t// Resolved through the registry, like every other reader of a gated resource, so a\n\t\t// `piConfig.configDir` override cannot point this at a file the classifier does not\n\t\t// check. Not trust-gated: `mcp auth <server>` is the user naming one server to\n\t\t// authorize, which is the decision, and there is no session trust state in a\n\t\t// one-shot command to consult instead.\n\t\tprojectPath: projectResourcePathByName(options.cwd ?? process.cwd(), PROJECT_CONFIG_FILENAME),\n\t\tglobalPath: globalMcpConfigPath(options.agentDir),\n\t});\n\tconst server = servers.get(options.serverName);\n\tif (!server) {\n\t\tconst names = [...servers.keys()];\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured. Configured servers: ${names.join(\", \") || \"none\"}. Add it to .mcp.json first.`,\n\t\t);\n\t}\n\tif (server.transport.kind !== \"http\") {\n\t\tthrow new Error(`MCP server \"${options.serverName}\" is a stdio server; OAuth applies to HTTP servers only.`);\n\t}\n\tif (server.auth !== \"oauth\") {\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured for OAuth. Add \"auth\": \"oauth\" to its entry.`,\n\t\t);\n\t}\n\tconst result = await (options.flow ?? runMcpOAuthFlow)({\n\t\tserverName: options.serverName,\n\t\tserverUrl: server.transport.url,\n\t\tcredentials: options.credentials ?? AuthStorage.create(),\n\t\tdeps: { print: options.log, openBrowser: options.openBrowser ?? (() => {}) },\n\t});\n\toptions.log(\n\t\t`Authorized \"${options.serverName}\". Token stored; expires ${new Date(result.expiresAt).toISOString()}.`,\n\t);\n}\n"]}
|
|
@@ -4,13 +4,18 @@
|
|
|
4
4
|
* OAuth-configured HTTP server, and runs the flow. UI-agnostic: callers supply the
|
|
5
5
|
* line printer and the browser opener.
|
|
6
6
|
*/
|
|
7
|
-
import { join } from "node:path";
|
|
8
7
|
import { AuthStorage } from "../../auth-storage.js";
|
|
8
|
+
import { projectResourcePathByName } from "../../project-resources.js";
|
|
9
9
|
import { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from "../config.js";
|
|
10
10
|
import { runMcpOAuthFlow } from "./flow.js";
|
|
11
11
|
export async function authorizeConfiguredServer(options) {
|
|
12
12
|
const { servers } = loadMcpConfig({
|
|
13
|
-
|
|
13
|
+
// Resolved through the registry, like every other reader of a gated resource, so a
|
|
14
|
+
// `piConfig.configDir` override cannot point this at a file the classifier does not
|
|
15
|
+
// check. Not trust-gated: `mcp auth <server>` is the user naming one server to
|
|
16
|
+
// authorize, which is the decision, and there is no session trust state in a
|
|
17
|
+
// one-shot command to consult instead.
|
|
18
|
+
projectPath: projectResourcePathByName(options.cwd ?? process.cwd(), PROJECT_CONFIG_FILENAME),
|
|
14
19
|
globalPath: globalMcpConfigPath(options.agentDir),
|
|
15
20
|
});
|
|
16
21
|
const server = servers.get(options.serverName);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../../../src/core/mcp/oauth/authorize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAgB5C,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,OAA+B,EAAiB;IAC/F,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC;QACjC,mFAAmF;QACnF,oFAAoF;QACpF,+EAA+E;QAC/E,6EAA6E;QAC7E,uCAAuC;QACvC,WAAW,EAAE,yBAAyB,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,uBAAuB,CAAC;QAC7F,UAAU,EAAE,mBAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC;KACjD,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QAClC,MAAM,IAAI,KAAK,CACd,eAAe,OAAO,CAAC,UAAU,4CAA4C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,8BAA8B,CACrI,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,eAAe,OAAO,CAAC,UAAU,0DAA0D,CAAC,CAAC;IAC9G,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACd,eAAe,OAAO,CAAC,UAAU,kEAAkE,CACnG,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,eAAe,CAAC,CAAC;QACtD,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,GAAG;QAC/B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,WAAW,CAAC,MAAM,EAAE;QACxD,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,EAAE;KAC5E,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,CACV,eAAe,OAAO,CAAC,UAAU,4BAA4B,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,GAAG,CACxG,CAAC;AAAA,CACF","sourcesContent":["/**\n * Shared entry point for `apex-code mcp auth <server>` and the TUI `/mcp auth`\n * command. Resolves the named server from the merged MCP config, checks it is an\n * OAuth-configured HTTP server, and runs the flow. UI-agnostic: callers supply the\n * line printer and the browser opener.\n */\n\nimport type { CredentialStore } from \"@earendil-works/pi-ai\";\nimport { AuthStorage } from \"../../auth-storage.ts\";\nimport { projectResourcePathByName } from \"../../project-resources.ts\";\nimport { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from \"../config.ts\";\nimport { runMcpOAuthFlow } from \"./flow.ts\";\n\nexport interface AuthorizeServerOptions {\n\tserverName: string;\n\t/** Project root for `.mcp.json` discovery. Default: process.cwd(). */\n\tcwd?: string;\n\t/** Agent directory the user-scope `mcp.json` is resolved against. Default: the host's. */\n\tagentDir?: string;\n\t/** Credential sink. Default: the host store (`AuthStorage.create()`). */\n\tcredentials?: CredentialStore;\n\topenBrowser?: (url: string) => void;\n\t/** Injectable flow, for tests. */\n\tflow?: typeof runMcpOAuthFlow;\n\tlog: (line: string) => void;\n}\n\nexport async function authorizeConfiguredServer(options: AuthorizeServerOptions): Promise<void> {\n\tconst { servers } = loadMcpConfig({\n\t\t// Resolved through the registry, like every other reader of a gated resource, so a\n\t\t// `piConfig.configDir` override cannot point this at a file the classifier does not\n\t\t// check. Not trust-gated: `mcp auth <server>` is the user naming one server to\n\t\t// authorize, which is the decision, and there is no session trust state in a\n\t\t// one-shot command to consult instead.\n\t\tprojectPath: projectResourcePathByName(options.cwd ?? process.cwd(), PROJECT_CONFIG_FILENAME),\n\t\tglobalPath: globalMcpConfigPath(options.agentDir),\n\t});\n\tconst server = servers.get(options.serverName);\n\tif (!server) {\n\t\tconst names = [...servers.keys()];\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured. Configured servers: ${names.join(\", \") || \"none\"}. Add it to .mcp.json first.`,\n\t\t);\n\t}\n\tif (server.transport.kind !== \"http\") {\n\t\tthrow new Error(`MCP server \"${options.serverName}\" is a stdio server; OAuth applies to HTTP servers only.`);\n\t}\n\tif (server.auth !== \"oauth\") {\n\t\tthrow new Error(\n\t\t\t`MCP server \"${options.serverName}\" is not configured for OAuth. Add \"auth\": \"oauth\" to its entry.`,\n\t\t);\n\t}\n\tconst result = await (options.flow ?? runMcpOAuthFlow)({\n\t\tserverName: options.serverName,\n\t\tserverUrl: server.transport.url,\n\t\tcredentials: options.credentials ?? AuthStorage.create(),\n\t\tdeps: { print: options.log, openBrowser: options.openBrowser ?? (() => {}) },\n\t});\n\toptions.log(\n\t\t`Authorized \"${options.serverName}\". Token stored; expires ${new Date(result.expiresAt).toISOString()}.`,\n\t);\n}\n"]}
|
|
@@ -14,8 +14,8 @@ export interface McpRuntime extends McpToolOptions {
|
|
|
14
14
|
warm(): Promise<void>;
|
|
15
15
|
close(): Promise<void>;
|
|
16
16
|
}
|
|
17
|
-
export declare function createMcpRuntime(cwd: string, connector
|
|
18
|
-
projectTrusted
|
|
17
|
+
export declare function createMcpRuntime(cwd: string, connector: McpConnector | undefined, options: {
|
|
18
|
+
projectTrusted: boolean;
|
|
19
19
|
agentDir?: string;
|
|
20
20
|
}): McpRuntime | undefined;
|
|
21
21
|
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,WAAW,UAAW,SAAQ,cAAc;IACjD,WAAW,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC5C,oFAAoF;IACpF,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,WAAW,UAAW,SAAQ,cAAc;IACjD,WAAW,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC5C,oFAAoF;IACpF,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,0BAAiC,EAC1C,OAAO,EAAE;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD,UAAU,GAAG,SAAS,CA8BxB","sourcesContent":["/**\n * Assembles the MCP subsystem for one session, or returns undefined when nothing is\n * configured.\n *\n * The undefined case is the point. A session with no `.mcp.json` builds no cache, no\n * manager, and no tool, so its registry and static prompt prefix are identical to a\n * build from before this subsystem existed.\n */\n\nimport { projectResourcePathByName } from \"../project-resources.ts\";\nimport { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from \"./config.ts\";\nimport { connectMcpServer } from \"./connector.ts\";\nimport type { McpToolOptions } from \"./mcp-tool.ts\";\nimport { McpMetadataCache } from \"./metadata-cache.ts\";\nimport { McpServerManager } from \"./server-manager.ts\";\nimport type { McpConfigDiagnostic, McpConnector } from \"./types.ts\";\n\nexport interface McpRuntime extends McpToolOptions {\n\tdiagnostics: readonly McpConfigDiagnostic[];\n\t/** Connect `eager` servers and cache their tools, so they are discoverable cold. */\n\twarm(): Promise<void>;\n\tclose(): Promise<void>;\n}\n\nexport function createMcpRuntime(\n\tcwd: string,\n\tconnector: McpConnector = connectMcpServer,\n\toptions: { projectTrusted: boolean; agentDir?: string },\n): McpRuntime | undefined {\n\t// ADR 0034. Inferring trust from `!== false` meant a forgotten argument and a deliberate\n\t// grant were the same value, and the forgotten one started the project's eager servers.\n\tif (typeof options?.projectTrusted !== \"boolean\") {\n\t\tthrow new TypeError(\"createMcpRuntime requires an explicit projectTrusted decision (ADR 0034)\");\n\t}\n\tconst { servers, diagnostics } = loadMcpConfig({\n\t\tprojectPath: options.projectTrusted ? projectResourcePathByName(cwd, PROJECT_CONFIG_FILENAME) : undefined,\n\t\tglobalPath: globalMcpConfigPath(options.agentDir),\n\t});\n\tif (servers.size === 0) return undefined;\n\n\tconst cache = new McpMetadataCache();\n\t// A server removed from config stops appearing in search immediately, rather than\n\t// lingering until something happens to overwrite its cache entry.\n\tcache.prune([...servers.values()]);\n\n\tconst manager = new McpServerManager({ servers, connector });\n\n\treturn {\n\t\tservers,\n\t\tcache,\n\t\tmanager,\n\t\tdiagnostics,\n\t\twarm: async () => {\n\t\t\tawait manager.warmEagerServers((server, tools) => cache.set(server, tools));\n\t\t\tcache.save();\n\t\t},\n\t\tclose: () => manager.closeAll(),\n\t};\n}\n"]}
|
package/dist/core/mcp/runtime.js
CHANGED
|
@@ -6,14 +6,19 @@
|
|
|
6
6
|
* manager, and no tool, so its registry and static prompt prefix are identical to a
|
|
7
7
|
* build from before this subsystem existed.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { projectResourcePathByName } from "../project-resources.js";
|
|
10
10
|
import { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from "./config.js";
|
|
11
11
|
import { connectMcpServer } from "./connector.js";
|
|
12
12
|
import { McpMetadataCache } from "./metadata-cache.js";
|
|
13
13
|
import { McpServerManager } from "./server-manager.js";
|
|
14
|
-
export function createMcpRuntime(cwd, connector = connectMcpServer, options
|
|
14
|
+
export function createMcpRuntime(cwd, connector = connectMcpServer, options) {
|
|
15
|
+
// ADR 0034. Inferring trust from `!== false` meant a forgotten argument and a deliberate
|
|
16
|
+
// grant were the same value, and the forgotten one started the project's eager servers.
|
|
17
|
+
if (typeof options?.projectTrusted !== "boolean") {
|
|
18
|
+
throw new TypeError("createMcpRuntime requires an explicit projectTrusted decision (ADR 0034)");
|
|
19
|
+
}
|
|
15
20
|
const { servers, diagnostics } = loadMcpConfig({
|
|
16
|
-
projectPath: options.projectTrusted
|
|
21
|
+
projectPath: options.projectTrusted ? projectResourcePathByName(cwd, PROJECT_CONFIG_FILENAME) : undefined,
|
|
17
22
|
globalPath: globalMcpConfigPath(options.agentDir),
|
|
18
23
|
});
|
|
19
24
|
if (servers.size === 0)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/core/mcp/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/core/mcp/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAUvD,MAAM,UAAU,gBAAgB,CAC/B,GAAW,EACX,SAAS,GAAiB,gBAAgB,EAC1C,OAAuD,EAC9B;IACzB,yFAAyF;IACzF,wFAAwF;IACxF,IAAI,OAAO,OAAO,EAAE,cAAc,KAAK,SAAS,EAAE,CAAC;QAClD,MAAM,IAAI,SAAS,CAAC,0EAA0E,CAAC,CAAC;IACjG,CAAC;IACD,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,aAAa,CAAC;QAC9C,WAAW,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,yBAAyB,CAAC,GAAG,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAC,SAAS;QACzG,UAAU,EAAE,mBAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC;KACjD,CAAC,CAAC;IACH,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAEzC,MAAM,KAAK,GAAG,IAAI,gBAAgB,EAAE,CAAC;IACrC,kFAAkF;IAClF,kEAAkE;IAClE,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAEnC,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;IAE7D,OAAO;QACN,OAAO;QACP,KAAK;QACL,OAAO;QACP,WAAW;QACX,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YACjB,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;YAC5E,KAAK,CAAC,IAAI,EAAE,CAAC;QAAA,CACb;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE;KAC/B,CAAC;AAAA,CACF","sourcesContent":["/**\n * Assembles the MCP subsystem for one session, or returns undefined when nothing is\n * configured.\n *\n * The undefined case is the point. A session with no `.mcp.json` builds no cache, no\n * manager, and no tool, so its registry and static prompt prefix are identical to a\n * build from before this subsystem existed.\n */\n\nimport { projectResourcePathByName } from \"../project-resources.ts\";\nimport { globalMcpConfigPath, loadMcpConfig, PROJECT_CONFIG_FILENAME } from \"./config.ts\";\nimport { connectMcpServer } from \"./connector.ts\";\nimport type { McpToolOptions } from \"./mcp-tool.ts\";\nimport { McpMetadataCache } from \"./metadata-cache.ts\";\nimport { McpServerManager } from \"./server-manager.ts\";\nimport type { McpConfigDiagnostic, McpConnector } from \"./types.ts\";\n\nexport interface McpRuntime extends McpToolOptions {\n\tdiagnostics: readonly McpConfigDiagnostic[];\n\t/** Connect `eager` servers and cache their tools, so they are discoverable cold. */\n\twarm(): Promise<void>;\n\tclose(): Promise<void>;\n}\n\nexport function createMcpRuntime(\n\tcwd: string,\n\tconnector: McpConnector = connectMcpServer,\n\toptions: { projectTrusted: boolean; agentDir?: string },\n): McpRuntime | undefined {\n\t// ADR 0034. Inferring trust from `!== false` meant a forgotten argument and a deliberate\n\t// grant were the same value, and the forgotten one started the project's eager servers.\n\tif (typeof options?.projectTrusted !== \"boolean\") {\n\t\tthrow new TypeError(\"createMcpRuntime requires an explicit projectTrusted decision (ADR 0034)\");\n\t}\n\tconst { servers, diagnostics } = loadMcpConfig({\n\t\tprojectPath: options.projectTrusted ? projectResourcePathByName(cwd, PROJECT_CONFIG_FILENAME) : undefined,\n\t\tglobalPath: globalMcpConfigPath(options.agentDir),\n\t});\n\tif (servers.size === 0) return undefined;\n\n\tconst cache = new McpMetadataCache();\n\t// A server removed from config stops appearing in search immediately, rather than\n\t// lingering until something happens to overwrite its cache entry.\n\tcache.prune([...servers.values()]);\n\n\tconst manager = new McpServerManager({ servers, connector });\n\n\treturn {\n\t\tservers,\n\t\tcache,\n\t\tmanager,\n\t\tdiagnostics,\n\t\twarm: async () => {\n\t\t\tawait manager.warmEagerServers((server, tools) => cache.set(server, tools));\n\t\t\tcache.save();\n\t\t},\n\t\tclose: () => manager.closeAll(),\n\t};\n}\n"]}
|
|
@@ -72,8 +72,13 @@ export interface CreateFilePermissionRuleStoreOptions {
|
|
|
72
72
|
backends?: Partial<Record<FileBackedSource, AuthStorageBackend>>;
|
|
73
73
|
/** Immutable argv layers (`flag` / `cliArg`), validated by the CLI before construction. */
|
|
74
74
|
initialRules?: readonly PermissionRule[];
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Resolved before construction. False excludes both project-controlled scopes.
|
|
77
|
+
*
|
|
78
|
+
* Required, per ADR 0034. There is no correct default: a caller that has not resolved
|
|
79
|
+
* trust cannot be given either answer about an untrusted checkout.
|
|
80
|
+
*/
|
|
81
|
+
projectTrusted: boolean;
|
|
77
82
|
}
|
|
78
83
|
/** File-backed store for policy/local/project/user, in-memory for command/session. */
|
|
79
84
|
export declare class FilePermissionRuleStore implements PermissionRuleStore {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Persistence for the permission rule model (ADR 0004). Four of the eight sources\n * are file-backed (policy, local, project, user); the other four are runtime-only\n * (flag, cliArg — parsed once from argv, not stored here; command, session — held\n * in memory for the life of the process). `session`-source updates are never\n * written to disk: \"allow for this session\" dies with the session by design.\n *\n * Reuses the locked-JSON-file backend already proven for Phase 1's operational\n * state (FileAuthStorageBackend) rather than widening `SettingsScope`, which only\n * covers two of these four file-backed sources and represents a different, larger\n * settings domain.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { type AuthStorageBackend, FileAuthStorageBackend } from \"../auth-storage.ts\";\nimport {\n\tPROJECT_LOCAL_PERMISSIONS_FILE,\n\tPROJECT_PERMISSIONS_FILE,\n\tprojectResourcePathByName,\n} from \"../project-resources.ts\";\nimport type { PermissionBehavior } from \"../tools/contract.ts\";\nimport type { PermissionRule, PermissionSource } from \"./rules.ts\";\n\nexport const PERMISSION_MODES = [\"default\", \"plan\", \"acceptEdits\", \"bypassPermissions\", \"dontAsk\"] as const;\nexport type PermissionMode = (typeof PERMISSION_MODES)[number];\n\n/** Sources a PermissionUpdate may target. Excludes `policy` (administrator-managed,\n * never user-writable) and `flag`/`cliArg` (parsed once from argv at startup, not\n * something this store updates after the fact). */\nexport const WRITABLE_PERMISSION_SOURCES = [\"local\", \"project\", \"user\", \"command\", \"session\"] as const;\nexport type WritablePermissionSource = (typeof WRITABLE_PERMISSION_SOURCES)[number];\n\nconst FILE_BACKED_SOURCES = [\"local\", \"project\", \"user\"] as const;\ntype FileBackedSource = (typeof FILE_BACKED_SOURCES)[number];\n\nconst RUNTIME_SOURCES = [\"command\", \"session\"] as const;\ntype RuntimeSource = (typeof RUNTIME_SOURCES)[number];\n\n/** A rule as persisted: `source` is never stored — it is implied by which file it came from. */\nexport type StoredPermissionRule = Omit<PermissionRule, \"source\">;\n\nexport interface RuleMatcher {\n\ttoolName: string;\n\truleContent?: string;\n}\n\nexport type PermissionUpdate =\n\t| { type: \"addRules\"; destination: WritablePermissionSource; rules: readonly StoredPermissionRule[] }\n\t| { type: \"replaceRules\"; destination: WritablePermissionSource; rules: readonly StoredPermissionRule[] }\n\t| { type: \"removeRules\"; destination: WritablePermissionSource; matching: readonly RuleMatcher[] }\n\t| { type: \"setMode\"; destination: WritablePermissionSource; mode: PermissionMode };\n\nexport interface PermissionStoreError {\n\tsource: PermissionSource;\n\terror: Error;\n}\n\nexport interface PermissionStoreSnapshot {\n\trules: readonly PermissionRule[];\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>;\n\terrors: readonly PermissionStoreError[];\n}\n\nexport interface PermissionRuleStore {\n\tsnapshot(): Promise<PermissionStoreSnapshot>;\n\tapply(update: PermissionUpdate): Promise<void>;\n}\n\nconst STORE_VERSION = 1;\n\ninterface StoredPermissionScope {\n\tversion: number;\n\trules: StoredPermissionRule[];\n\tmode?: PermissionMode;\n}\n\nfunction emptyScope(): StoredPermissionScope {\n\treturn { version: STORE_VERSION, rules: [] };\n}\n\n/**\n * Whether a permission file's contents grant anything. The trust classifier asks this\n * so a `{}` file does not raise a prompt that carries no decision. Unparseable content\n * counts as granting: a malformed or hostile file must prompt rather than pass.\n */\nexport function permissionContentConfersAuthority(content: string | undefined): boolean {\n\tlet scope: StoredPermissionScope;\n\ttry {\n\t\tscope = parseScope(content);\n\t} catch {\n\t\treturn true;\n\t}\n\treturn scope.rules.length > 0 || scope.mode !== undefined;\n}\n\nfunction isPermissionBehavior(value: unknown): value is PermissionBehavior {\n\treturn value === \"allow\" || value === \"deny\" || value === \"ask\";\n}\n\nfunction parseScope(content: string | undefined): StoredPermissionScope {\n\tif (!content) return emptyScope();\n\tconst parsed = JSON.parse(content);\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new Error(\"Invalid permission rules file: unexpected shape\");\n\t}\n\t// FileAuthStorageBackend (shared with auth.json) seeds a freshly created file\n\t// with the literal text \"{}\". Treat a content-free object as \"nothing written\n\t// yet\" rather than malformed; a real malformed file has some recognizable but\n\t// wrong-shaped field, caught below.\n\tif (!(\"version\" in parsed) && !(\"rules\" in parsed) && !(\"mode\" in parsed)) {\n\t\treturn emptyScope();\n\t}\n\tif (\n\t\tparsed.version !== STORE_VERSION ||\n\t\t(parsed.mode !== undefined && !(PERMISSION_MODES as readonly string[]).includes(parsed.mode)) ||\n\t\t!Array.isArray(parsed.rules) ||\n\t\t!parsed.rules.every(\n\t\t\t(rule: unknown) =>\n\t\t\t\ttypeof rule === \"object\" &&\n\t\t\t\trule !== null &&\n\t\t\t\ttypeof (rule as StoredPermissionRule).toolName === \"string\" &&\n\t\t\t\tisPermissionBehavior((rule as StoredPermissionRule).behavior) &&\n\t\t\t\t((rule as StoredPermissionRule).ruleContent === undefined ||\n\t\t\t\t\ttypeof (rule as StoredPermissionRule).ruleContent === \"string\"),\n\t\t)\n\t) {\n\t\tthrow new Error(\"Invalid permission rules file: unexpected shape\");\n\t}\n\treturn parsed as StoredPermissionScope;\n}\n\nfunction applyToScope(scope: StoredPermissionScope, update: PermissionUpdate): StoredPermissionScope {\n\tswitch (update.type) {\n\t\tcase \"addRules\":\n\t\t\treturn { ...scope, rules: [...scope.rules, ...update.rules] };\n\t\tcase \"replaceRules\":\n\t\t\treturn { ...scope, rules: [...update.rules] };\n\t\tcase \"removeRules\":\n\t\t\treturn {\n\t\t\t\t...scope,\n\t\t\t\trules: scope.rules.filter(\n\t\t\t\t\t(rule) =>\n\t\t\t\t\t\t!update.matching.some((m) => m.toolName === rule.toolName && m.ruleContent === rule.ruleContent),\n\t\t\t\t),\n\t\t\t};\n\t\tcase \"setMode\":\n\t\t\treturn { ...scope, mode: update.mode };\n\t}\n}\n\nfunction defaultPolicyPath(): string {\n\tif (process.env.APEX_CODE_POLICY_PATH) return process.env.APEX_CODE_POLICY_PATH;\n\treturn process.platform === \"win32\"\n\t\t? join(process.env.ProgramData ?? \"C:\\\\ProgramData\", \"apex-code\", \"policy.json\")\n\t\t: \"/etc/apex-code/policy.json\";\n}\n\nexport interface CreateFilePermissionRuleStoreOptions {\n\tcwd: string;\n\tagentDir?: string;\n\t/** Overrides the managed policy file path. Defaults to $APEX_CODE_POLICY_PATH, else an OS-specific system location. */\n\tpolicyPath?: string;\n\t/** Test seam: inject backends for the three writable file-backed sources instead of touching real files. */\n\tbackends?: Partial<Record<FileBackedSource, AuthStorageBackend>>;\n\t/** Immutable argv layers (`flag` / `cliArg`), validated by the CLI before construction. */\n\tinitialRules?: readonly PermissionRule[];\n\t/** Resolved before construction. False excludes both project-controlled scopes. */\n\tprojectTrusted?: boolean;\n}\n\n/** File-backed store for policy/local/project/user, in-memory for command/session. */\nexport class FilePermissionRuleStore implements PermissionRuleStore {\n\tprivate readonly backends: Record<FileBackedSource, AuthStorageBackend>;\n\tprivate readonly policyPath: string;\n\tprivate readonly initialRules: readonly PermissionRule[];\n\tprivate readonly runtimeRules: Record<RuntimeSource, StoredPermissionRule[]> = { command: [], session: [] };\n\tprivate readonly runtimeModes: Partial<Record<RuntimeSource, PermissionMode>> = {};\n\tprivate readonly projectTrusted: boolean;\n\tprivate readonly fileScopes: Partial<\n\t\tRecord<FileBackedSource, Promise<{ scope: StoredPermissionScope; error?: Error }>>\n\t> = {};\n\tprivate readonly fileScopeUpdates: Partial<Record<FileBackedSource, Promise<void>>> = {};\n\n\tconstructor(options: CreateFilePermissionRuleStoreOptions) {\n\t\tconst agentDir = options.agentDir ?? getAgentDir();\n\t\tconst cwd = resolvePath(options.cwd);\n\t\tthis.backends = {\n\t\t\tuser: new FileAuthStorageBackend(join(agentDir, \"permissions.json\")),\n\t\t\tproject: new FileAuthStorageBackend(projectResourcePathByName(cwd, PROJECT_PERMISSIONS_FILE)),\n\t\t\tlocal: new FileAuthStorageBackend(projectResourcePathByName(cwd, PROJECT_LOCAL_PERMISSIONS_FILE)),\n\t\t\t...options.backends,\n\t\t};\n\t\tthis.policyPath = options.policyPath ?? defaultPolicyPath();\n\t\tthis.initialRules = options.initialRules ?? [];\n\t\tthis.projectTrusted = options.projectTrusted ?? true;\n\t\tif (!this.projectTrusted) {\n\t\t\tthis.fileScopes.local = Promise.resolve({ scope: emptyScope() });\n\t\t\tthis.fileScopes.project = Promise.resolve({ scope: emptyScope() });\n\t\t}\n\t}\n\n\tprivate capturedFileBackedScope(source: FileBackedSource): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\tconst captured = this.fileScopes[source];\n\t\tif (captured) return captured;\n\t\tconst initial = this.readFileBackedScope(source);\n\t\tthis.fileScopes[source] = initial;\n\t\treturn initial;\n\t}\n\n\tprivate async readFileBackedScope(\n\t\tsource: FileBackedSource,\n\t): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\ttry {\n\t\t\tlet content: string | undefined;\n\t\t\tawait this.backends[source].withLockAsync(async (current) => {\n\t\t\t\tcontent = current;\n\t\t\t\treturn { result: undefined };\n\t\t\t});\n\t\t\treturn { scope: parseScope(content) };\n\t\t} catch (error) {\n\t\t\treturn { scope: emptyScope(), error: error instanceof Error ? error : new Error(String(error)) };\n\t\t}\n\t}\n\n\tprivate async readPolicy(): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\ttry {\n\t\t\tconst content = await readFile(this.policyPath, \"utf-8\");\n\t\t\treturn { scope: parseScope(content) };\n\t\t} catch (error) {\n\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return { scope: emptyScope() };\n\t\t\treturn { scope: emptyScope(), error: error instanceof Error ? error : new Error(String(error)) };\n\t\t}\n\t}\n\n\tprivate capturedPolicyAndUser(): Promise<{\n\t\tpolicy: { scope: StoredPermissionScope; error?: Error };\n\t\tuser: { scope: StoredPermissionScope; error?: Error };\n\t}> {\n\t\treturn Promise.all([this.readPolicy(), this.capturedFileBackedScope(\"user\")]).then(([policy, user]) => ({\n\t\t\tpolicy,\n\t\t\tuser,\n\t\t}));\n\t}\n\n\tasync snapshot(): Promise<PermissionStoreSnapshot> {\n\t\t// File-backed authorization is captured once. Direct filesystem writes,\n\t\t// including symlink replacement, cannot widen the current or next gate\n\t\t// decision. Managed policy and runtime scopes remain live.\n\t\tconst [policyAndUser, local, project] = await Promise.all([\n\t\t\tthis.capturedPolicyAndUser(),\n\t\t\tthis.capturedFileBackedScope(\"local\"),\n\t\t\tthis.capturedFileBackedScope(\"project\"),\n\t\t]);\n\t\tconst { policy, user } = policyAndUser;\n\t\tconst errors: PermissionStoreError[] = [];\n\t\tconst withSource = (\n\t\t\tsource: PermissionSource,\n\t\t\tentry: { scope: StoredPermissionScope; error?: Error },\n\t\t): PermissionRule[] => {\n\t\t\tif (entry.error) errors.push({ source, error: entry.error });\n\t\t\treturn entry.scope.rules.map((rule) => ({ ...rule, source }));\n\t\t};\n\n\t\tconst rules: PermissionRule[] = [\n\t\t\t...withSource(\"policy\", policy),\n\t\t\t...this.initialRules,\n\t\t\t...withSource(\"local\", local),\n\t\t\t...withSource(\"project\", project),\n\t\t\t...withSource(\"user\", user),\n\t\t\t...this.runtimeRules.command.map((rule) => ({ ...rule, source: \"command\" as const })),\n\t\t\t...this.runtimeRules.session.map((rule) => ({ ...rule, source: \"session\" as const })),\n\t\t];\n\n\t\tconst modesBySource = new Map<WritablePermissionSource, PermissionMode>();\n\t\tif (local.scope.mode) modesBySource.set(\"local\", local.scope.mode);\n\t\tif (project.scope.mode) modesBySource.set(\"project\", project.scope.mode);\n\t\tif (user.scope.mode) modesBySource.set(\"user\", user.scope.mode);\n\t\tif (this.runtimeModes.command) modesBySource.set(\"command\", this.runtimeModes.command);\n\t\tif (this.runtimeModes.session) modesBySource.set(\"session\", this.runtimeModes.session);\n\n\t\treturn { rules, modesBySource, errors };\n\t}\n\n\tasync apply(update: PermissionUpdate): Promise<void> {\n\t\tif (update.destination === \"command\" || update.destination === \"session\") {\n\t\t\tthis.applyRuntime(update.destination, update);\n\t\t\treturn;\n\t\t}\n\t\tif (!this.projectTrusted && (update.destination === \"local\" || update.destination === \"project\")) {\n\t\t\tthrow new Error(`Cannot update untrusted ${update.destination} permission scope`);\n\t\t}\n\t\tconst destination = update.destination;\n\t\tconst previousUpdate = this.fileScopeUpdates[destination]?.catch(() => undefined) ?? Promise.resolve();\n\t\tconst nextUpdate = previousUpdate.then(async () => {\n\t\t\tconst current = await this.capturedFileBackedScope(destination);\n\t\t\tconst next = applyToScope(current.scope, update);\n\t\t\tawait this.backends[destination].withLockAsync(async () => ({\n\t\t\t\tresult: undefined,\n\t\t\t\tnext: JSON.stringify(next, null, 2),\n\t\t\t}));\n\t\t\tthis.fileScopes[destination] = Promise.resolve({ scope: next });\n\t\t});\n\t\tthis.fileScopeUpdates[destination] = nextUpdate;\n\t\tawait nextUpdate;\n\t}\n\n\tprivate applyRuntime(destination: RuntimeSource, update: PermissionUpdate): void {\n\t\tif (update.type === \"setMode\") {\n\t\t\tthis.runtimeModes[destination] = update.mode;\n\t\t\treturn;\n\t\t}\n\t\tconst current: StoredPermissionScope = { version: STORE_VERSION, rules: this.runtimeRules[destination] };\n\t\tthis.runtimeRules[destination] = applyToScope(current, update).rules;\n\t}\n}\n\nexport interface DerivedPermissionRuleStoreOptions {\n\t/** The parent's live store. Read fresh on every `snapshot()`, never cached, so a\n\t * rule the parent picks up mid-session (e.g. a human's \"always allow\") is visible\n\t * to the child on its very next call. */\n\tparent: PermissionRuleStore;\n}\n\n/**\n * A delegated child's permission store (roadmap Phase 5, ADR 0008). A read-through\n * view over the parent's live snapshot -- including its runtime-only sources\n * (`flag`, `cliArg`, `command`, `session`), which a subprocess-spawned child could\n * never observe -- plus a child-local runtime overlay for the child's own writes.\n *\n * `apply()` never reaches the parent. It writes only into this instance's own\n * overlay, and only for the two runtime-only writable sources (`command`, `session`);\n * a file-backed destination (`local`, `project`, `user`) is rejected outright, not\n * silently redirected -- a delegated child must never be able to persist to the\n * parent's settings files, and \"runtime-only\" is enforced by construction here, not\n * by a convention a future caller could forget. The overlay is discarded with the\n * child: there is no code path from a child's approval back into the parent's store,\n * which is what keeps a child's `persist: true` from widening its parent.\n */\nexport class DerivedPermissionRuleStore implements PermissionRuleStore {\n\tprivate readonly parent: PermissionRuleStore;\n\tprivate readonly overlayRules: Record<RuntimeSource, StoredPermissionRule[]> = { command: [], session: [] };\n\tprivate readonly overlayModes: Partial<Record<RuntimeSource, PermissionMode>> = {};\n\n\tconstructor(options: DerivedPermissionRuleStoreOptions) {\n\t\tthis.parent = options.parent;\n\t}\n\n\tasync snapshot(): Promise<PermissionStoreSnapshot> {\n\t\tconst parentSnapshot = await this.parent.snapshot();\n\t\tconst rules: PermissionRule[] = [\n\t\t\t...parentSnapshot.rules,\n\t\t\t...this.overlayRules.command.map((rule) => ({ ...rule, source: \"command\" as const })),\n\t\t\t...this.overlayRules.session.map((rule) => ({ ...rule, source: \"session\" as const })),\n\t\t];\n\t\tconst modesBySource = new Map(parentSnapshot.modesBySource);\n\t\tif (this.overlayModes.command !== undefined) modesBySource.set(\"command\", this.overlayModes.command);\n\t\tif (this.overlayModes.session !== undefined) modesBySource.set(\"session\", this.overlayModes.session);\n\t\treturn { rules, modesBySource, errors: parentSnapshot.errors };\n\t}\n\n\tasync apply(update: PermissionUpdate): Promise<void> {\n\t\tif (update.destination !== \"command\" && update.destination !== \"session\") {\n\t\t\tthrow new Error(\n\t\t\t\t`A delegated child cannot persist permission rules to \"${update.destination}\" -- only \"command\" and \"session\" are writable from a derived store (ADR 0008).`,\n\t\t\t);\n\t\t}\n\t\tif (update.type === \"setMode\") {\n\t\t\tthis.overlayModes[update.destination] = update.mode;\n\t\t\treturn;\n\t\t}\n\t\tconst current: StoredPermissionScope = {\n\t\t\tversion: STORE_VERSION,\n\t\t\trules: this.overlayRules[update.destination],\n\t\t};\n\t\tthis.overlayRules[update.destination] = applyToScope(current, update).rules;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Persistence for the permission rule model (ADR 0004). Four of the eight sources\n * are file-backed (policy, local, project, user); the other four are runtime-only\n * (flag, cliArg — parsed once from argv, not stored here; command, session — held\n * in memory for the life of the process). `session`-source updates are never\n * written to disk: \"allow for this session\" dies with the session by design.\n *\n * Reuses the locked-JSON-file backend already proven for Phase 1's operational\n * state (FileAuthStorageBackend) rather than widening `SettingsScope`, which only\n * covers two of these four file-backed sources and represents a different, larger\n * settings domain.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { type AuthStorageBackend, FileAuthStorageBackend } from \"../auth-storage.ts\";\nimport {\n\tPROJECT_LOCAL_PERMISSIONS_FILE,\n\tPROJECT_PERMISSIONS_FILE,\n\tprojectResourcePathByName,\n} from \"../project-resources.ts\";\nimport type { PermissionBehavior } from \"../tools/contract.ts\";\nimport type { PermissionRule, PermissionSource } from \"./rules.ts\";\n\nexport const PERMISSION_MODES = [\"default\", \"plan\", \"acceptEdits\", \"bypassPermissions\", \"dontAsk\"] as const;\nexport type PermissionMode = (typeof PERMISSION_MODES)[number];\n\n/** Sources a PermissionUpdate may target. Excludes `policy` (administrator-managed,\n * never user-writable) and `flag`/`cliArg` (parsed once from argv at startup, not\n * something this store updates after the fact). */\nexport const WRITABLE_PERMISSION_SOURCES = [\"local\", \"project\", \"user\", \"command\", \"session\"] as const;\nexport type WritablePermissionSource = (typeof WRITABLE_PERMISSION_SOURCES)[number];\n\nconst FILE_BACKED_SOURCES = [\"local\", \"project\", \"user\"] as const;\ntype FileBackedSource = (typeof FILE_BACKED_SOURCES)[number];\n\nconst RUNTIME_SOURCES = [\"command\", \"session\"] as const;\ntype RuntimeSource = (typeof RUNTIME_SOURCES)[number];\n\n/** A rule as persisted: `source` is never stored — it is implied by which file it came from. */\nexport type StoredPermissionRule = Omit<PermissionRule, \"source\">;\n\nexport interface RuleMatcher {\n\ttoolName: string;\n\truleContent?: string;\n}\n\nexport type PermissionUpdate =\n\t| { type: \"addRules\"; destination: WritablePermissionSource; rules: readonly StoredPermissionRule[] }\n\t| { type: \"replaceRules\"; destination: WritablePermissionSource; rules: readonly StoredPermissionRule[] }\n\t| { type: \"removeRules\"; destination: WritablePermissionSource; matching: readonly RuleMatcher[] }\n\t| { type: \"setMode\"; destination: WritablePermissionSource; mode: PermissionMode };\n\nexport interface PermissionStoreError {\n\tsource: PermissionSource;\n\terror: Error;\n}\n\nexport interface PermissionStoreSnapshot {\n\trules: readonly PermissionRule[];\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>;\n\terrors: readonly PermissionStoreError[];\n}\n\nexport interface PermissionRuleStore {\n\tsnapshot(): Promise<PermissionStoreSnapshot>;\n\tapply(update: PermissionUpdate): Promise<void>;\n}\n\nconst STORE_VERSION = 1;\n\ninterface StoredPermissionScope {\n\tversion: number;\n\trules: StoredPermissionRule[];\n\tmode?: PermissionMode;\n}\n\nfunction emptyScope(): StoredPermissionScope {\n\treturn { version: STORE_VERSION, rules: [] };\n}\n\n/**\n * Whether a permission file's contents grant anything. The trust classifier asks this\n * so a `{}` file does not raise a prompt that carries no decision. Unparseable content\n * counts as granting: a malformed or hostile file must prompt rather than pass.\n */\nexport function permissionContentConfersAuthority(content: string | undefined): boolean {\n\tlet scope: StoredPermissionScope;\n\ttry {\n\t\tscope = parseScope(content);\n\t} catch {\n\t\treturn true;\n\t}\n\treturn scope.rules.length > 0 || scope.mode !== undefined;\n}\n\nfunction isPermissionBehavior(value: unknown): value is PermissionBehavior {\n\treturn value === \"allow\" || value === \"deny\" || value === \"ask\";\n}\n\nfunction parseScope(content: string | undefined): StoredPermissionScope {\n\tif (!content) return emptyScope();\n\tconst parsed = JSON.parse(content);\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new Error(\"Invalid permission rules file: unexpected shape\");\n\t}\n\t// FileAuthStorageBackend (shared with auth.json) seeds a freshly created file\n\t// with the literal text \"{}\". Treat a content-free object as \"nothing written\n\t// yet\" rather than malformed; a real malformed file has some recognizable but\n\t// wrong-shaped field, caught below.\n\tif (!(\"version\" in parsed) && !(\"rules\" in parsed) && !(\"mode\" in parsed)) {\n\t\treturn emptyScope();\n\t}\n\tif (\n\t\tparsed.version !== STORE_VERSION ||\n\t\t(parsed.mode !== undefined && !(PERMISSION_MODES as readonly string[]).includes(parsed.mode)) ||\n\t\t!Array.isArray(parsed.rules) ||\n\t\t!parsed.rules.every(\n\t\t\t(rule: unknown) =>\n\t\t\t\ttypeof rule === \"object\" &&\n\t\t\t\trule !== null &&\n\t\t\t\ttypeof (rule as StoredPermissionRule).toolName === \"string\" &&\n\t\t\t\tisPermissionBehavior((rule as StoredPermissionRule).behavior) &&\n\t\t\t\t((rule as StoredPermissionRule).ruleContent === undefined ||\n\t\t\t\t\ttypeof (rule as StoredPermissionRule).ruleContent === \"string\"),\n\t\t)\n\t) {\n\t\tthrow new Error(\"Invalid permission rules file: unexpected shape\");\n\t}\n\treturn parsed as StoredPermissionScope;\n}\n\nfunction applyToScope(scope: StoredPermissionScope, update: PermissionUpdate): StoredPermissionScope {\n\tswitch (update.type) {\n\t\tcase \"addRules\":\n\t\t\treturn { ...scope, rules: [...scope.rules, ...update.rules] };\n\t\tcase \"replaceRules\":\n\t\t\treturn { ...scope, rules: [...update.rules] };\n\t\tcase \"removeRules\":\n\t\t\treturn {\n\t\t\t\t...scope,\n\t\t\t\trules: scope.rules.filter(\n\t\t\t\t\t(rule) =>\n\t\t\t\t\t\t!update.matching.some((m) => m.toolName === rule.toolName && m.ruleContent === rule.ruleContent),\n\t\t\t\t),\n\t\t\t};\n\t\tcase \"setMode\":\n\t\t\treturn { ...scope, mode: update.mode };\n\t}\n}\n\nfunction defaultPolicyPath(): string {\n\tif (process.env.APEX_CODE_POLICY_PATH) return process.env.APEX_CODE_POLICY_PATH;\n\treturn process.platform === \"win32\"\n\t\t? join(process.env.ProgramData ?? \"C:\\\\ProgramData\", \"apex-code\", \"policy.json\")\n\t\t: \"/etc/apex-code/policy.json\";\n}\n\nexport interface CreateFilePermissionRuleStoreOptions {\n\tcwd: string;\n\tagentDir?: string;\n\t/** Overrides the managed policy file path. Defaults to $APEX_CODE_POLICY_PATH, else an OS-specific system location. */\n\tpolicyPath?: string;\n\t/** Test seam: inject backends for the three writable file-backed sources instead of touching real files. */\n\tbackends?: Partial<Record<FileBackedSource, AuthStorageBackend>>;\n\t/** Immutable argv layers (`flag` / `cliArg`), validated by the CLI before construction. */\n\tinitialRules?: readonly PermissionRule[];\n\t/**\n\t * Resolved before construction. False excludes both project-controlled scopes.\n\t *\n\t * Required, per ADR 0034. There is no correct default: a caller that has not resolved\n\t * trust cannot be given either answer about an untrusted checkout.\n\t */\n\tprojectTrusted: boolean;\n}\n\n/** File-backed store for policy/local/project/user, in-memory for command/session. */\nexport class FilePermissionRuleStore implements PermissionRuleStore {\n\tprivate readonly backends: Record<FileBackedSource, AuthStorageBackend>;\n\tprivate readonly policyPath: string;\n\tprivate readonly initialRules: readonly PermissionRule[];\n\tprivate readonly runtimeRules: Record<RuntimeSource, StoredPermissionRule[]> = { command: [], session: [] };\n\tprivate readonly runtimeModes: Partial<Record<RuntimeSource, PermissionMode>> = {};\n\tprivate readonly projectTrusted: boolean;\n\tprivate readonly fileScopes: Partial<\n\t\tRecord<FileBackedSource, Promise<{ scope: StoredPermissionScope; error?: Error }>>\n\t> = {};\n\tprivate readonly fileScopeUpdates: Partial<Record<FileBackedSource, Promise<void>>> = {};\n\n\tconstructor(options: CreateFilePermissionRuleStoreOptions) {\n\t\tconst agentDir = options.agentDir ?? getAgentDir();\n\t\tconst cwd = resolvePath(options.cwd);\n\t\tthis.backends = {\n\t\t\tuser: new FileAuthStorageBackend(join(agentDir, \"permissions.json\")),\n\t\t\tproject: new FileAuthStorageBackend(projectResourcePathByName(cwd, PROJECT_PERMISSIONS_FILE)),\n\t\t\tlocal: new FileAuthStorageBackend(projectResourcePathByName(cwd, PROJECT_LOCAL_PERMISSIONS_FILE)),\n\t\t\t...options.backends,\n\t\t};\n\t\tthis.policyPath = options.policyPath ?? defaultPolicyPath();\n\t\tthis.initialRules = options.initialRules ?? [];\n\t\t// A JavaScript caller reaches this with no compiler in between, so the type is not\n\t\t// the whole guard. ADR 0034.\n\t\tif (typeof options.projectTrusted !== \"boolean\") {\n\t\t\tthrow new TypeError(\"FilePermissionRuleStore requires an explicit projectTrusted decision (ADR 0034)\");\n\t\t}\n\t\tthis.projectTrusted = options.projectTrusted;\n\t\tif (!this.projectTrusted) {\n\t\t\tthis.fileScopes.local = Promise.resolve({ scope: emptyScope() });\n\t\t\tthis.fileScopes.project = Promise.resolve({ scope: emptyScope() });\n\t\t}\n\t}\n\n\tprivate capturedFileBackedScope(source: FileBackedSource): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\tconst captured = this.fileScopes[source];\n\t\tif (captured) return captured;\n\t\tconst initial = this.readFileBackedScope(source);\n\t\tthis.fileScopes[source] = initial;\n\t\treturn initial;\n\t}\n\n\tprivate async readFileBackedScope(\n\t\tsource: FileBackedSource,\n\t): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\ttry {\n\t\t\tlet content: string | undefined;\n\t\t\tawait this.backends[source].withLockAsync(async (current) => {\n\t\t\t\tcontent = current;\n\t\t\t\treturn { result: undefined };\n\t\t\t});\n\t\t\treturn { scope: parseScope(content) };\n\t\t} catch (error) {\n\t\t\treturn { scope: emptyScope(), error: error instanceof Error ? error : new Error(String(error)) };\n\t\t}\n\t}\n\n\tprivate async readPolicy(): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\ttry {\n\t\t\tconst content = await readFile(this.policyPath, \"utf-8\");\n\t\t\treturn { scope: parseScope(content) };\n\t\t} catch (error) {\n\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return { scope: emptyScope() };\n\t\t\treturn { scope: emptyScope(), error: error instanceof Error ? error : new Error(String(error)) };\n\t\t}\n\t}\n\n\tprivate capturedPolicyAndUser(): Promise<{\n\t\tpolicy: { scope: StoredPermissionScope; error?: Error };\n\t\tuser: { scope: StoredPermissionScope; error?: Error };\n\t}> {\n\t\treturn Promise.all([this.readPolicy(), this.capturedFileBackedScope(\"user\")]).then(([policy, user]) => ({\n\t\t\tpolicy,\n\t\t\tuser,\n\t\t}));\n\t}\n\n\tasync snapshot(): Promise<PermissionStoreSnapshot> {\n\t\t// File-backed authorization is captured once. Direct filesystem writes,\n\t\t// including symlink replacement, cannot widen the current or next gate\n\t\t// decision. Managed policy and runtime scopes remain live.\n\t\tconst [policyAndUser, local, project] = await Promise.all([\n\t\t\tthis.capturedPolicyAndUser(),\n\t\t\tthis.capturedFileBackedScope(\"local\"),\n\t\t\tthis.capturedFileBackedScope(\"project\"),\n\t\t]);\n\t\tconst { policy, user } = policyAndUser;\n\t\tconst errors: PermissionStoreError[] = [];\n\t\tconst withSource = (\n\t\t\tsource: PermissionSource,\n\t\t\tentry: { scope: StoredPermissionScope; error?: Error },\n\t\t): PermissionRule[] => {\n\t\t\tif (entry.error) errors.push({ source, error: entry.error });\n\t\t\treturn entry.scope.rules.map((rule) => ({ ...rule, source }));\n\t\t};\n\n\t\tconst rules: PermissionRule[] = [\n\t\t\t...withSource(\"policy\", policy),\n\t\t\t...this.initialRules,\n\t\t\t...withSource(\"local\", local),\n\t\t\t...withSource(\"project\", project),\n\t\t\t...withSource(\"user\", user),\n\t\t\t...this.runtimeRules.command.map((rule) => ({ ...rule, source: \"command\" as const })),\n\t\t\t...this.runtimeRules.session.map((rule) => ({ ...rule, source: \"session\" as const })),\n\t\t];\n\n\t\tconst modesBySource = new Map<WritablePermissionSource, PermissionMode>();\n\t\tif (local.scope.mode) modesBySource.set(\"local\", local.scope.mode);\n\t\tif (project.scope.mode) modesBySource.set(\"project\", project.scope.mode);\n\t\tif (user.scope.mode) modesBySource.set(\"user\", user.scope.mode);\n\t\tif (this.runtimeModes.command) modesBySource.set(\"command\", this.runtimeModes.command);\n\t\tif (this.runtimeModes.session) modesBySource.set(\"session\", this.runtimeModes.session);\n\n\t\treturn { rules, modesBySource, errors };\n\t}\n\n\tasync apply(update: PermissionUpdate): Promise<void> {\n\t\tif (update.destination === \"command\" || update.destination === \"session\") {\n\t\t\tthis.applyRuntime(update.destination, update);\n\t\t\treturn;\n\t\t}\n\t\tif (!this.projectTrusted && (update.destination === \"local\" || update.destination === \"project\")) {\n\t\t\tthrow new Error(`Cannot update untrusted ${update.destination} permission scope`);\n\t\t}\n\t\tconst destination = update.destination;\n\t\tconst previousUpdate = this.fileScopeUpdates[destination]?.catch(() => undefined) ?? Promise.resolve();\n\t\tconst nextUpdate = previousUpdate.then(async () => {\n\t\t\tconst current = await this.capturedFileBackedScope(destination);\n\t\t\tconst next = applyToScope(current.scope, update);\n\t\t\tawait this.backends[destination].withLockAsync(async () => ({\n\t\t\t\tresult: undefined,\n\t\t\t\tnext: JSON.stringify(next, null, 2),\n\t\t\t}));\n\t\t\tthis.fileScopes[destination] = Promise.resolve({ scope: next });\n\t\t});\n\t\tthis.fileScopeUpdates[destination] = nextUpdate;\n\t\tawait nextUpdate;\n\t}\n\n\tprivate applyRuntime(destination: RuntimeSource, update: PermissionUpdate): void {\n\t\tif (update.type === \"setMode\") {\n\t\t\tthis.runtimeModes[destination] = update.mode;\n\t\t\treturn;\n\t\t}\n\t\tconst current: StoredPermissionScope = { version: STORE_VERSION, rules: this.runtimeRules[destination] };\n\t\tthis.runtimeRules[destination] = applyToScope(current, update).rules;\n\t}\n}\n\nexport interface DerivedPermissionRuleStoreOptions {\n\t/** The parent's live store. Read fresh on every `snapshot()`, never cached, so a\n\t * rule the parent picks up mid-session (e.g. a human's \"always allow\") is visible\n\t * to the child on its very next call. */\n\tparent: PermissionRuleStore;\n}\n\n/**\n * A delegated child's permission store (roadmap Phase 5, ADR 0008). A read-through\n * view over the parent's live snapshot -- including its runtime-only sources\n * (`flag`, `cliArg`, `command`, `session`), which a subprocess-spawned child could\n * never observe -- plus a child-local runtime overlay for the child's own writes.\n *\n * `apply()` never reaches the parent. It writes only into this instance's own\n * overlay, and only for the two runtime-only writable sources (`command`, `session`);\n * a file-backed destination (`local`, `project`, `user`) is rejected outright, not\n * silently redirected -- a delegated child must never be able to persist to the\n * parent's settings files, and \"runtime-only\" is enforced by construction here, not\n * by a convention a future caller could forget. The overlay is discarded with the\n * child: there is no code path from a child's approval back into the parent's store,\n * which is what keeps a child's `persist: true` from widening its parent.\n */\nexport class DerivedPermissionRuleStore implements PermissionRuleStore {\n\tprivate readonly parent: PermissionRuleStore;\n\tprivate readonly overlayRules: Record<RuntimeSource, StoredPermissionRule[]> = { command: [], session: [] };\n\tprivate readonly overlayModes: Partial<Record<RuntimeSource, PermissionMode>> = {};\n\n\tconstructor(options: DerivedPermissionRuleStoreOptions) {\n\t\tthis.parent = options.parent;\n\t}\n\n\tasync snapshot(): Promise<PermissionStoreSnapshot> {\n\t\tconst parentSnapshot = await this.parent.snapshot();\n\t\tconst rules: PermissionRule[] = [\n\t\t\t...parentSnapshot.rules,\n\t\t\t...this.overlayRules.command.map((rule) => ({ ...rule, source: \"command\" as const })),\n\t\t\t...this.overlayRules.session.map((rule) => ({ ...rule, source: \"session\" as const })),\n\t\t];\n\t\tconst modesBySource = new Map(parentSnapshot.modesBySource);\n\t\tif (this.overlayModes.command !== undefined) modesBySource.set(\"command\", this.overlayModes.command);\n\t\tif (this.overlayModes.session !== undefined) modesBySource.set(\"session\", this.overlayModes.session);\n\t\treturn { rules, modesBySource, errors: parentSnapshot.errors };\n\t}\n\n\tasync apply(update: PermissionUpdate): Promise<void> {\n\t\tif (update.destination !== \"command\" && update.destination !== \"session\") {\n\t\t\tthrow new Error(\n\t\t\t\t`A delegated child cannot persist permission rules to \"${update.destination}\" -- only \"command\" and \"session\" are writable from a derived store (ADR 0008).`,\n\t\t\t);\n\t\t}\n\t\tif (update.type === \"setMode\") {\n\t\t\tthis.overlayModes[update.destination] = update.mode;\n\t\t\treturn;\n\t\t}\n\t\tconst current: StoredPermissionScope = {\n\t\t\tversion: STORE_VERSION,\n\t\t\trules: this.overlayRules[update.destination],\n\t\t};\n\t\tthis.overlayRules[update.destination] = applyToScope(current, update).rules;\n\t}\n}\n"]}
|
|
@@ -115,7 +115,12 @@ export class FilePermissionRuleStore {
|
|
|
115
115
|
};
|
|
116
116
|
this.policyPath = options.policyPath ?? defaultPolicyPath();
|
|
117
117
|
this.initialRules = options.initialRules ?? [];
|
|
118
|
-
this
|
|
118
|
+
// A JavaScript caller reaches this with no compiler in between, so the type is not
|
|
119
|
+
// the whole guard. ADR 0034.
|
|
120
|
+
if (typeof options.projectTrusted !== "boolean") {
|
|
121
|
+
throw new TypeError("FilePermissionRuleStore requires an explicit projectTrusted decision (ADR 0034)");
|
|
122
|
+
}
|
|
123
|
+
this.projectTrusted = options.projectTrusted;
|
|
119
124
|
if (!this.projectTrusted) {
|
|
120
125
|
this.fileScopes.local = Promise.resolve({ scope: emptyScope() });
|
|
121
126
|
this.fileScopes.project = Promise.resolve({ scope: emptyScope() });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/core/permissions/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAA2B,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EACN,8BAA8B,EAC9B,wBAAwB,EACxB,yBAAyB,GACzB,MAAM,yBAAyB,CAAC;AAIjC,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,mBAAmB,EAAE,SAAS,CAAU,CAAC;AAG5G;;mDAEmD;AACnD,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,CAAU,CAAC;AAGvG,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,CAAU,CAAC;AAGlE,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,SAAS,CAAU,CAAC;AAiCxD,MAAM,aAAa,GAAG,CAAC,CAAC;AAQxB,SAAS,UAAU,GAA0B;IAC5C,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AAAA,CAC7C;AAED;;;;GAIG;AACH,MAAM,UAAU,iCAAiC,CAAC,OAA2B,EAAW;IACvF,IAAI,KAA4B,CAAC;IACjC,IAAI,CAAC;QACJ,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC;AAAA,CAC1D;AAED,SAAS,oBAAoB,CAAC,KAAc,EAA+B;IAC1E,OAAO,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,KAAK,CAAC;AAAA,CAChE;AAED,SAAS,UAAU,CAAC,OAA2B,EAAyB;IACvE,IAAI,CAAC,OAAO;QAAE,OAAO,UAAU,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACpE,CAAC;IACD,8EAA8E;IAC9E,8EAA8E;IAC9E,8EAA8E;IAC9E,oCAAoC;IACpC,IAAI,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,EAAE,CAAC;QAC3E,OAAO,UAAU,EAAE,CAAC;IACrB,CAAC;IACD,IACC,MAAM,CAAC,OAAO,KAAK,aAAa;QAChC,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,CAAE,gBAAsC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7F,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;QAC5B,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAClB,CAAC,IAAa,EAAE,EAAE,CACjB,OAAO,IAAI,KAAK,QAAQ;YACxB,IAAI,KAAK,IAAI;YACb,OAAQ,IAA6B,CAAC,QAAQ,KAAK,QAAQ;YAC3D,oBAAoB,CAAE,IAA6B,CAAC,QAAQ,CAAC;YAC7D,CAAE,IAA6B,CAAC,WAAW,KAAK,SAAS;gBACxD,OAAQ,IAA6B,CAAC,WAAW,KAAK,QAAQ,CAAC,CACjE,EACA,CAAC;QACF,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,MAA+B,CAAC;AAAA,CACvC;AAED,SAAS,YAAY,CAAC,KAA4B,EAAE,MAAwB,EAAyB;IACpG,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACrB,KAAK,UAAU;YACd,OAAO,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/D,KAAK,cAAc;YAClB,OAAO,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,KAAK,aAAa;YACjB,OAAO;gBACN,GAAG,KAAK;gBACR,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CACxB,CAAC,IAAI,EAAE,EAAE,CACR,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,WAAW,KAAK,IAAI,CAAC,WAAW,CAAC,CACjG;aACD,CAAC;QACH,KAAK,SAAS;YACb,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;IACzC,CAAC;AAAA,CACD;AAED,SAAS,iBAAiB,GAAW;IACpC,IAAI,OAAO,CAAC,GAAG,CAAC,qBAAqB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;IAChF,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO;QAClC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,iBAAiB,EAAE,WAAW,EAAE,aAAa,CAAC;QAChF,CAAC,CAAC,4BAA4B,CAAC;AAAA,CAChC;AAeD,sFAAsF;AACtF,MAAM,OAAO,uBAAuB;IAClB,QAAQ,CAA+C;IACvD,UAAU,CAAS;IACnB,YAAY,CAA4B;IACxC,YAAY,GAAkD,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC3F,YAAY,GAAmD,EAAE,CAAC;IAClE,cAAc,CAAU;IACxB,UAAU,GAEvB,EAAE,CAAC;IACU,gBAAgB,GAAqD,EAAE,CAAC;IAEzF,YAAY,OAA6C,EAAE;QAC1D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,WAAW,EAAE,CAAC;QACnD,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG;YACf,IAAI,EAAE,IAAI,sBAAsB,CAAC,IAAI,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;YACpE,OAAO,EAAE,IAAI,sBAAsB,CAAC,yBAAyB,CAAC,GAAG,EAAE,wBAAwB,CAAC,CAAC;YAC7F,KAAK,EAAE,IAAI,sBAAsB,CAAC,yBAAyB,CAAC,GAAG,EAAE,8BAA8B,CAAC,CAAC;YACjG,GAAG,OAAO,CAAC,QAAQ;SACnB,CAAC;QACF,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,iBAAiB,EAAE,CAAC;QAC5D,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QAC/C,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,IAAI,CAAC;QACrD,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YAC1B,IAAI,CAAC,UAAU,CAAC,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;QACpE,CAAC;IAAA,CACD;IAEO,uBAAuB,CAAC,MAAwB,EAA4D;QACnH,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;QAClC,OAAO,OAAO,CAAC;IAAA,CACf;IAEO,KAAK,CAAC,mBAAmB,CAChC,MAAwB,EACmC;QAC3D,IAAI,CAAC;YACJ,IAAI,OAA2B,CAAC;YAChC,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;gBAC5D,OAAO,GAAG,OAAO,CAAC;gBAClB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;YAAA,CAC7B,CAAC,CAAC;YACH,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QAClG,CAAC;IAAA,CACD;IAEO,KAAK,CAAC,UAAU,GAA6D;QACpF,IAAI,CAAC;YACJ,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YACzD,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;YACvF,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QAClG,CAAC;IAAA,CACD;IAEO,qBAAqB,GAG1B;QACF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;YACvG,MAAM;YACN,IAAI;SACJ,CAAC,CAAC,CAAC;IAAA,CACJ;IAED,KAAK,CAAC,QAAQ,GAAqC;QAClD,wEAAwE;QACxE,uEAAuE;QACvE,2DAA2D;QAC3D,MAAM,CAAC,aAAa,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACzD,IAAI,CAAC,qBAAqB,EAAE;YAC5B,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC;YACrC,IAAI,CAAC,uBAAuB,CAAC,SAAS,CAAC;SACvC,CAAC,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC;QACvC,MAAM,MAAM,GAA2B,EAAE,CAAC;QAC1C,MAAM,UAAU,GAAG,CAClB,MAAwB,EACxB,KAAsD,EACnC,EAAE,CAAC;YACtB,IAAI,KAAK,CAAC,KAAK;gBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;YAC7D,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;QAAA,CAC9D,CAAC;QAEF,MAAM,KAAK,GAAqB;YAC/B,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;YAC/B,GAAG,IAAI,CAAC,YAAY;YACpB,GAAG,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC;YAC7B,GAAG,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC;YACjC,GAAG,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC;YAC3B,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,SAAkB,EAAE,CAAC,CAAC;YACrF,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,SAAkB,EAAE,CAAC,CAAC;SACrF,CAAC;QAEF,MAAM,aAAa,GAAG,IAAI,GAAG,EAA4C,CAAC;QAC1E,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI;YAAE,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnE,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI;YAAE,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChE,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACvF,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAEvF,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;IAAA,CACxC;IAED,KAAK,CAAC,KAAK,CAAC,MAAwB,EAAiB;QACpD,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YAC1E,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAC9C,OAAO;QACR,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,cAAc,IAAI,CAAC,MAAM,CAAC,WAAW,KAAK,OAAO,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,CAAC,EAAE,CAAC;YAClG,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,CAAC,WAAW,mBAAmB,CAAC,CAAC;QACnF,CAAC;QACD,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;QACvC,MAAM,cAAc,GAAG,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACvG,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAClD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,WAAW,CAAC,CAAC;YAChE,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACjD,MAAM,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,aAAa,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;gBAC3D,MAAM,EAAE,SAAS;gBACjB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;aACnC,CAAC,CAAC,CAAC;YACJ,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAAA,CAChE,CAAC,CAAC;QACH,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC;QAChD,MAAM,UAAU,CAAC;IAAA,CACjB;IAEO,YAAY,CAAC,WAA0B,EAAE,MAAwB,EAAQ;QAChF,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;YAC7C,OAAO;QACR,CAAC;QACD,MAAM,OAAO,GAA0B,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,EAAE,CAAC;QACzG,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC;IAAA,CACrE;CACD;AASD;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,0BAA0B;IACrB,MAAM,CAAsB;IAC5B,YAAY,GAAkD,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC3F,YAAY,GAAmD,EAAE,CAAC;IAEnF,YAAY,OAA0C,EAAE;QACvD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAAA,CAC7B;IAED,KAAK,CAAC,QAAQ,GAAqC;QAClD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpD,MAAM,KAAK,GAAqB;YAC/B,GAAG,cAAc,CAAC,KAAK;YACvB,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,SAAkB,EAAE,CAAC,CAAC;YACrF,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,SAAkB,EAAE,CAAC,CAAC;SACrF,CAAC;QACF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,KAAK,SAAS;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACrG,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,KAAK,SAAS;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACrG,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,CAAC,MAAM,EAAE,CAAC;IAAA,CAC/D;IAED,KAAK,CAAC,KAAK,CAAC,MAAwB,EAAiB;QACpD,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YAC1E,MAAM,IAAI,KAAK,CACd,yDAAyD,MAAM,CAAC,WAAW,iFAAiF,CAC5J,CAAC;QACH,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;YACpD,OAAO;QACR,CAAC;QACD,MAAM,OAAO,GAA0B;YACtC,OAAO,EAAE,aAAa;YACtB,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC;SAC5C,CAAC;QACF,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC;IAAA,CAC5E;CACD","sourcesContent":["/**\n * Persistence for the permission rule model (ADR 0004). Four of the eight sources\n * are file-backed (policy, local, project, user); the other four are runtime-only\n * (flag, cliArg — parsed once from argv, not stored here; command, session — held\n * in memory for the life of the process). `session`-source updates are never\n * written to disk: \"allow for this session\" dies with the session by design.\n *\n * Reuses the locked-JSON-file backend already proven for Phase 1's operational\n * state (FileAuthStorageBackend) rather than widening `SettingsScope`, which only\n * covers two of these four file-backed sources and represents a different, larger\n * settings domain.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { type AuthStorageBackend, FileAuthStorageBackend } from \"../auth-storage.ts\";\nimport {\n\tPROJECT_LOCAL_PERMISSIONS_FILE,\n\tPROJECT_PERMISSIONS_FILE,\n\tprojectResourcePathByName,\n} from \"../project-resources.ts\";\nimport type { PermissionBehavior } from \"../tools/contract.ts\";\nimport type { PermissionRule, PermissionSource } from \"./rules.ts\";\n\nexport const PERMISSION_MODES = [\"default\", \"plan\", \"acceptEdits\", \"bypassPermissions\", \"dontAsk\"] as const;\nexport type PermissionMode = (typeof PERMISSION_MODES)[number];\n\n/** Sources a PermissionUpdate may target. Excludes `policy` (administrator-managed,\n * never user-writable) and `flag`/`cliArg` (parsed once from argv at startup, not\n * something this store updates after the fact). */\nexport const WRITABLE_PERMISSION_SOURCES = [\"local\", \"project\", \"user\", \"command\", \"session\"] as const;\nexport type WritablePermissionSource = (typeof WRITABLE_PERMISSION_SOURCES)[number];\n\nconst FILE_BACKED_SOURCES = [\"local\", \"project\", \"user\"] as const;\ntype FileBackedSource = (typeof FILE_BACKED_SOURCES)[number];\n\nconst RUNTIME_SOURCES = [\"command\", \"session\"] as const;\ntype RuntimeSource = (typeof RUNTIME_SOURCES)[number];\n\n/** A rule as persisted: `source` is never stored — it is implied by which file it came from. */\nexport type StoredPermissionRule = Omit<PermissionRule, \"source\">;\n\nexport interface RuleMatcher {\n\ttoolName: string;\n\truleContent?: string;\n}\n\nexport type PermissionUpdate =\n\t| { type: \"addRules\"; destination: WritablePermissionSource; rules: readonly StoredPermissionRule[] }\n\t| { type: \"replaceRules\"; destination: WritablePermissionSource; rules: readonly StoredPermissionRule[] }\n\t| { type: \"removeRules\"; destination: WritablePermissionSource; matching: readonly RuleMatcher[] }\n\t| { type: \"setMode\"; destination: WritablePermissionSource; mode: PermissionMode };\n\nexport interface PermissionStoreError {\n\tsource: PermissionSource;\n\terror: Error;\n}\n\nexport interface PermissionStoreSnapshot {\n\trules: readonly PermissionRule[];\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>;\n\terrors: readonly PermissionStoreError[];\n}\n\nexport interface PermissionRuleStore {\n\tsnapshot(): Promise<PermissionStoreSnapshot>;\n\tapply(update: PermissionUpdate): Promise<void>;\n}\n\nconst STORE_VERSION = 1;\n\ninterface StoredPermissionScope {\n\tversion: number;\n\trules: StoredPermissionRule[];\n\tmode?: PermissionMode;\n}\n\nfunction emptyScope(): StoredPermissionScope {\n\treturn { version: STORE_VERSION, rules: [] };\n}\n\n/**\n * Whether a permission file's contents grant anything. The trust classifier asks this\n * so a `{}` file does not raise a prompt that carries no decision. Unparseable content\n * counts as granting: a malformed or hostile file must prompt rather than pass.\n */\nexport function permissionContentConfersAuthority(content: string | undefined): boolean {\n\tlet scope: StoredPermissionScope;\n\ttry {\n\t\tscope = parseScope(content);\n\t} catch {\n\t\treturn true;\n\t}\n\treturn scope.rules.length > 0 || scope.mode !== undefined;\n}\n\nfunction isPermissionBehavior(value: unknown): value is PermissionBehavior {\n\treturn value === \"allow\" || value === \"deny\" || value === \"ask\";\n}\n\nfunction parseScope(content: string | undefined): StoredPermissionScope {\n\tif (!content) return emptyScope();\n\tconst parsed = JSON.parse(content);\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new Error(\"Invalid permission rules file: unexpected shape\");\n\t}\n\t// FileAuthStorageBackend (shared with auth.json) seeds a freshly created file\n\t// with the literal text \"{}\". Treat a content-free object as \"nothing written\n\t// yet\" rather than malformed; a real malformed file has some recognizable but\n\t// wrong-shaped field, caught below.\n\tif (!(\"version\" in parsed) && !(\"rules\" in parsed) && !(\"mode\" in parsed)) {\n\t\treturn emptyScope();\n\t}\n\tif (\n\t\tparsed.version !== STORE_VERSION ||\n\t\t(parsed.mode !== undefined && !(PERMISSION_MODES as readonly string[]).includes(parsed.mode)) ||\n\t\t!Array.isArray(parsed.rules) ||\n\t\t!parsed.rules.every(\n\t\t\t(rule: unknown) =>\n\t\t\t\ttypeof rule === \"object\" &&\n\t\t\t\trule !== null &&\n\t\t\t\ttypeof (rule as StoredPermissionRule).toolName === \"string\" &&\n\t\t\t\tisPermissionBehavior((rule as StoredPermissionRule).behavior) &&\n\t\t\t\t((rule as StoredPermissionRule).ruleContent === undefined ||\n\t\t\t\t\ttypeof (rule as StoredPermissionRule).ruleContent === \"string\"),\n\t\t)\n\t) {\n\t\tthrow new Error(\"Invalid permission rules file: unexpected shape\");\n\t}\n\treturn parsed as StoredPermissionScope;\n}\n\nfunction applyToScope(scope: StoredPermissionScope, update: PermissionUpdate): StoredPermissionScope {\n\tswitch (update.type) {\n\t\tcase \"addRules\":\n\t\t\treturn { ...scope, rules: [...scope.rules, ...update.rules] };\n\t\tcase \"replaceRules\":\n\t\t\treturn { ...scope, rules: [...update.rules] };\n\t\tcase \"removeRules\":\n\t\t\treturn {\n\t\t\t\t...scope,\n\t\t\t\trules: scope.rules.filter(\n\t\t\t\t\t(rule) =>\n\t\t\t\t\t\t!update.matching.some((m) => m.toolName === rule.toolName && m.ruleContent === rule.ruleContent),\n\t\t\t\t),\n\t\t\t};\n\t\tcase \"setMode\":\n\t\t\treturn { ...scope, mode: update.mode };\n\t}\n}\n\nfunction defaultPolicyPath(): string {\n\tif (process.env.APEX_CODE_POLICY_PATH) return process.env.APEX_CODE_POLICY_PATH;\n\treturn process.platform === \"win32\"\n\t\t? join(process.env.ProgramData ?? \"C:\\\\ProgramData\", \"apex-code\", \"policy.json\")\n\t\t: \"/etc/apex-code/policy.json\";\n}\n\nexport interface CreateFilePermissionRuleStoreOptions {\n\tcwd: string;\n\tagentDir?: string;\n\t/** Overrides the managed policy file path. Defaults to $APEX_CODE_POLICY_PATH, else an OS-specific system location. */\n\tpolicyPath?: string;\n\t/** Test seam: inject backends for the three writable file-backed sources instead of touching real files. */\n\tbackends?: Partial<Record<FileBackedSource, AuthStorageBackend>>;\n\t/** Immutable argv layers (`flag` / `cliArg`), validated by the CLI before construction. */\n\tinitialRules?: readonly PermissionRule[];\n\t/** Resolved before construction. False excludes both project-controlled scopes. */\n\tprojectTrusted?: boolean;\n}\n\n/** File-backed store for policy/local/project/user, in-memory for command/session. */\nexport class FilePermissionRuleStore implements PermissionRuleStore {\n\tprivate readonly backends: Record<FileBackedSource, AuthStorageBackend>;\n\tprivate readonly policyPath: string;\n\tprivate readonly initialRules: readonly PermissionRule[];\n\tprivate readonly runtimeRules: Record<RuntimeSource, StoredPermissionRule[]> = { command: [], session: [] };\n\tprivate readonly runtimeModes: Partial<Record<RuntimeSource, PermissionMode>> = {};\n\tprivate readonly projectTrusted: boolean;\n\tprivate readonly fileScopes: Partial<\n\t\tRecord<FileBackedSource, Promise<{ scope: StoredPermissionScope; error?: Error }>>\n\t> = {};\n\tprivate readonly fileScopeUpdates: Partial<Record<FileBackedSource, Promise<void>>> = {};\n\n\tconstructor(options: CreateFilePermissionRuleStoreOptions) {\n\t\tconst agentDir = options.agentDir ?? getAgentDir();\n\t\tconst cwd = resolvePath(options.cwd);\n\t\tthis.backends = {\n\t\t\tuser: new FileAuthStorageBackend(join(agentDir, \"permissions.json\")),\n\t\t\tproject: new FileAuthStorageBackend(projectResourcePathByName(cwd, PROJECT_PERMISSIONS_FILE)),\n\t\t\tlocal: new FileAuthStorageBackend(projectResourcePathByName(cwd, PROJECT_LOCAL_PERMISSIONS_FILE)),\n\t\t\t...options.backends,\n\t\t};\n\t\tthis.policyPath = options.policyPath ?? defaultPolicyPath();\n\t\tthis.initialRules = options.initialRules ?? [];\n\t\tthis.projectTrusted = options.projectTrusted ?? true;\n\t\tif (!this.projectTrusted) {\n\t\t\tthis.fileScopes.local = Promise.resolve({ scope: emptyScope() });\n\t\t\tthis.fileScopes.project = Promise.resolve({ scope: emptyScope() });\n\t\t}\n\t}\n\n\tprivate capturedFileBackedScope(source: FileBackedSource): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\tconst captured = this.fileScopes[source];\n\t\tif (captured) return captured;\n\t\tconst initial = this.readFileBackedScope(source);\n\t\tthis.fileScopes[source] = initial;\n\t\treturn initial;\n\t}\n\n\tprivate async readFileBackedScope(\n\t\tsource: FileBackedSource,\n\t): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\ttry {\n\t\t\tlet content: string | undefined;\n\t\t\tawait this.backends[source].withLockAsync(async (current) => {\n\t\t\t\tcontent = current;\n\t\t\t\treturn { result: undefined };\n\t\t\t});\n\t\t\treturn { scope: parseScope(content) };\n\t\t} catch (error) {\n\t\t\treturn { scope: emptyScope(), error: error instanceof Error ? error : new Error(String(error)) };\n\t\t}\n\t}\n\n\tprivate async readPolicy(): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\ttry {\n\t\t\tconst content = await readFile(this.policyPath, \"utf-8\");\n\t\t\treturn { scope: parseScope(content) };\n\t\t} catch (error) {\n\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return { scope: emptyScope() };\n\t\t\treturn { scope: emptyScope(), error: error instanceof Error ? error : new Error(String(error)) };\n\t\t}\n\t}\n\n\tprivate capturedPolicyAndUser(): Promise<{\n\t\tpolicy: { scope: StoredPermissionScope; error?: Error };\n\t\tuser: { scope: StoredPermissionScope; error?: Error };\n\t}> {\n\t\treturn Promise.all([this.readPolicy(), this.capturedFileBackedScope(\"user\")]).then(([policy, user]) => ({\n\t\t\tpolicy,\n\t\t\tuser,\n\t\t}));\n\t}\n\n\tasync snapshot(): Promise<PermissionStoreSnapshot> {\n\t\t// File-backed authorization is captured once. Direct filesystem writes,\n\t\t// including symlink replacement, cannot widen the current or next gate\n\t\t// decision. Managed policy and runtime scopes remain live.\n\t\tconst [policyAndUser, local, project] = await Promise.all([\n\t\t\tthis.capturedPolicyAndUser(),\n\t\t\tthis.capturedFileBackedScope(\"local\"),\n\t\t\tthis.capturedFileBackedScope(\"project\"),\n\t\t]);\n\t\tconst { policy, user } = policyAndUser;\n\t\tconst errors: PermissionStoreError[] = [];\n\t\tconst withSource = (\n\t\t\tsource: PermissionSource,\n\t\t\tentry: { scope: StoredPermissionScope; error?: Error },\n\t\t): PermissionRule[] => {\n\t\t\tif (entry.error) errors.push({ source, error: entry.error });\n\t\t\treturn entry.scope.rules.map((rule) => ({ ...rule, source }));\n\t\t};\n\n\t\tconst rules: PermissionRule[] = [\n\t\t\t...withSource(\"policy\", policy),\n\t\t\t...this.initialRules,\n\t\t\t...withSource(\"local\", local),\n\t\t\t...withSource(\"project\", project),\n\t\t\t...withSource(\"user\", user),\n\t\t\t...this.runtimeRules.command.map((rule) => ({ ...rule, source: \"command\" as const })),\n\t\t\t...this.runtimeRules.session.map((rule) => ({ ...rule, source: \"session\" as const })),\n\t\t];\n\n\t\tconst modesBySource = new Map<WritablePermissionSource, PermissionMode>();\n\t\tif (local.scope.mode) modesBySource.set(\"local\", local.scope.mode);\n\t\tif (project.scope.mode) modesBySource.set(\"project\", project.scope.mode);\n\t\tif (user.scope.mode) modesBySource.set(\"user\", user.scope.mode);\n\t\tif (this.runtimeModes.command) modesBySource.set(\"command\", this.runtimeModes.command);\n\t\tif (this.runtimeModes.session) modesBySource.set(\"session\", this.runtimeModes.session);\n\n\t\treturn { rules, modesBySource, errors };\n\t}\n\n\tasync apply(update: PermissionUpdate): Promise<void> {\n\t\tif (update.destination === \"command\" || update.destination === \"session\") {\n\t\t\tthis.applyRuntime(update.destination, update);\n\t\t\treturn;\n\t\t}\n\t\tif (!this.projectTrusted && (update.destination === \"local\" || update.destination === \"project\")) {\n\t\t\tthrow new Error(`Cannot update untrusted ${update.destination} permission scope`);\n\t\t}\n\t\tconst destination = update.destination;\n\t\tconst previousUpdate = this.fileScopeUpdates[destination]?.catch(() => undefined) ?? Promise.resolve();\n\t\tconst nextUpdate = previousUpdate.then(async () => {\n\t\t\tconst current = await this.capturedFileBackedScope(destination);\n\t\t\tconst next = applyToScope(current.scope, update);\n\t\t\tawait this.backends[destination].withLockAsync(async () => ({\n\t\t\t\tresult: undefined,\n\t\t\t\tnext: JSON.stringify(next, null, 2),\n\t\t\t}));\n\t\t\tthis.fileScopes[destination] = Promise.resolve({ scope: next });\n\t\t});\n\t\tthis.fileScopeUpdates[destination] = nextUpdate;\n\t\tawait nextUpdate;\n\t}\n\n\tprivate applyRuntime(destination: RuntimeSource, update: PermissionUpdate): void {\n\t\tif (update.type === \"setMode\") {\n\t\t\tthis.runtimeModes[destination] = update.mode;\n\t\t\treturn;\n\t\t}\n\t\tconst current: StoredPermissionScope = { version: STORE_VERSION, rules: this.runtimeRules[destination] };\n\t\tthis.runtimeRules[destination] = applyToScope(current, update).rules;\n\t}\n}\n\nexport interface DerivedPermissionRuleStoreOptions {\n\t/** The parent's live store. Read fresh on every `snapshot()`, never cached, so a\n\t * rule the parent picks up mid-session (e.g. a human's \"always allow\") is visible\n\t * to the child on its very next call. */\n\tparent: PermissionRuleStore;\n}\n\n/**\n * A delegated child's permission store (roadmap Phase 5, ADR 0008). A read-through\n * view over the parent's live snapshot -- including its runtime-only sources\n * (`flag`, `cliArg`, `command`, `session`), which a subprocess-spawned child could\n * never observe -- plus a child-local runtime overlay for the child's own writes.\n *\n * `apply()` never reaches the parent. It writes only into this instance's own\n * overlay, and only for the two runtime-only writable sources (`command`, `session`);\n * a file-backed destination (`local`, `project`, `user`) is rejected outright, not\n * silently redirected -- a delegated child must never be able to persist to the\n * parent's settings files, and \"runtime-only\" is enforced by construction here, not\n * by a convention a future caller could forget. The overlay is discarded with the\n * child: there is no code path from a child's approval back into the parent's store,\n * which is what keeps a child's `persist: true` from widening its parent.\n */\nexport class DerivedPermissionRuleStore implements PermissionRuleStore {\n\tprivate readonly parent: PermissionRuleStore;\n\tprivate readonly overlayRules: Record<RuntimeSource, StoredPermissionRule[]> = { command: [], session: [] };\n\tprivate readonly overlayModes: Partial<Record<RuntimeSource, PermissionMode>> = {};\n\n\tconstructor(options: DerivedPermissionRuleStoreOptions) {\n\t\tthis.parent = options.parent;\n\t}\n\n\tasync snapshot(): Promise<PermissionStoreSnapshot> {\n\t\tconst parentSnapshot = await this.parent.snapshot();\n\t\tconst rules: PermissionRule[] = [\n\t\t\t...parentSnapshot.rules,\n\t\t\t...this.overlayRules.command.map((rule) => ({ ...rule, source: \"command\" as const })),\n\t\t\t...this.overlayRules.session.map((rule) => ({ ...rule, source: \"session\" as const })),\n\t\t];\n\t\tconst modesBySource = new Map(parentSnapshot.modesBySource);\n\t\tif (this.overlayModes.command !== undefined) modesBySource.set(\"command\", this.overlayModes.command);\n\t\tif (this.overlayModes.session !== undefined) modesBySource.set(\"session\", this.overlayModes.session);\n\t\treturn { rules, modesBySource, errors: parentSnapshot.errors };\n\t}\n\n\tasync apply(update: PermissionUpdate): Promise<void> {\n\t\tif (update.destination !== \"command\" && update.destination !== \"session\") {\n\t\t\tthrow new Error(\n\t\t\t\t`A delegated child cannot persist permission rules to \"${update.destination}\" -- only \"command\" and \"session\" are writable from a derived store (ADR 0008).`,\n\t\t\t);\n\t\t}\n\t\tif (update.type === \"setMode\") {\n\t\t\tthis.overlayModes[update.destination] = update.mode;\n\t\t\treturn;\n\t\t}\n\t\tconst current: StoredPermissionScope = {\n\t\t\tversion: STORE_VERSION,\n\t\t\trules: this.overlayRules[update.destination],\n\t\t};\n\t\tthis.overlayRules[update.destination] = applyToScope(current, update).rules;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/core/permissions/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAA2B,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EACN,8BAA8B,EAC9B,wBAAwB,EACxB,yBAAyB,GACzB,MAAM,yBAAyB,CAAC;AAIjC,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,mBAAmB,EAAE,SAAS,CAAU,CAAC;AAG5G;;mDAEmD;AACnD,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,CAAU,CAAC;AAGvG,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,CAAU,CAAC;AAGlE,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,SAAS,CAAU,CAAC;AAiCxD,MAAM,aAAa,GAAG,CAAC,CAAC;AAQxB,SAAS,UAAU,GAA0B;IAC5C,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AAAA,CAC7C;AAED;;;;GAIG;AACH,MAAM,UAAU,iCAAiC,CAAC,OAA2B,EAAW;IACvF,IAAI,KAA4B,CAAC;IACjC,IAAI,CAAC;QACJ,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC;AAAA,CAC1D;AAED,SAAS,oBAAoB,CAAC,KAAc,EAA+B;IAC1E,OAAO,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,KAAK,CAAC;AAAA,CAChE;AAED,SAAS,UAAU,CAAC,OAA2B,EAAyB;IACvE,IAAI,CAAC,OAAO;QAAE,OAAO,UAAU,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACpE,CAAC;IACD,8EAA8E;IAC9E,8EAA8E;IAC9E,8EAA8E;IAC9E,oCAAoC;IACpC,IAAI,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,EAAE,CAAC;QAC3E,OAAO,UAAU,EAAE,CAAC;IACrB,CAAC;IACD,IACC,MAAM,CAAC,OAAO,KAAK,aAAa;QAChC,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,CAAE,gBAAsC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7F,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;QAC5B,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAClB,CAAC,IAAa,EAAE,EAAE,CACjB,OAAO,IAAI,KAAK,QAAQ;YACxB,IAAI,KAAK,IAAI;YACb,OAAQ,IAA6B,CAAC,QAAQ,KAAK,QAAQ;YAC3D,oBAAoB,CAAE,IAA6B,CAAC,QAAQ,CAAC;YAC7D,CAAE,IAA6B,CAAC,WAAW,KAAK,SAAS;gBACxD,OAAQ,IAA6B,CAAC,WAAW,KAAK,QAAQ,CAAC,CACjE,EACA,CAAC;QACF,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,MAA+B,CAAC;AAAA,CACvC;AAED,SAAS,YAAY,CAAC,KAA4B,EAAE,MAAwB,EAAyB;IACpG,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACrB,KAAK,UAAU;YACd,OAAO,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/D,KAAK,cAAc;YAClB,OAAO,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,KAAK,aAAa;YACjB,OAAO;gBACN,GAAG,KAAK;gBACR,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CACxB,CAAC,IAAI,EAAE,EAAE,CACR,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,WAAW,KAAK,IAAI,CAAC,WAAW,CAAC,CACjG;aACD,CAAC;QACH,KAAK,SAAS;YACb,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;IACzC,CAAC;AAAA,CACD;AAED,SAAS,iBAAiB,GAAW;IACpC,IAAI,OAAO,CAAC,GAAG,CAAC,qBAAqB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;IAChF,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO;QAClC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,iBAAiB,EAAE,WAAW,EAAE,aAAa,CAAC;QAChF,CAAC,CAAC,4BAA4B,CAAC;AAAA,CAChC;AAoBD,sFAAsF;AACtF,MAAM,OAAO,uBAAuB;IAClB,QAAQ,CAA+C;IACvD,UAAU,CAAS;IACnB,YAAY,CAA4B;IACxC,YAAY,GAAkD,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC3F,YAAY,GAAmD,EAAE,CAAC;IAClE,cAAc,CAAU;IACxB,UAAU,GAEvB,EAAE,CAAC;IACU,gBAAgB,GAAqD,EAAE,CAAC;IAEzF,YAAY,OAA6C,EAAE;QAC1D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,WAAW,EAAE,CAAC;QACnD,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG;YACf,IAAI,EAAE,IAAI,sBAAsB,CAAC,IAAI,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;YACpE,OAAO,EAAE,IAAI,sBAAsB,CAAC,yBAAyB,CAAC,GAAG,EAAE,wBAAwB,CAAC,CAAC;YAC7F,KAAK,EAAE,IAAI,sBAAsB,CAAC,yBAAyB,CAAC,GAAG,EAAE,8BAA8B,CAAC,CAAC;YACjG,GAAG,OAAO,CAAC,QAAQ;SACnB,CAAC;QACF,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,iBAAiB,EAAE,CAAC;QAC5D,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QAC/C,mFAAmF;QACnF,6BAA6B;QAC7B,IAAI,OAAO,OAAO,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;YACjD,MAAM,IAAI,SAAS,CAAC,iFAAiF,CAAC,CAAC;QACxG,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;QAC7C,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YAC1B,IAAI,CAAC,UAAU,CAAC,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;QACpE,CAAC;IAAA,CACD;IAEO,uBAAuB,CAAC,MAAwB,EAA4D;QACnH,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;QAClC,OAAO,OAAO,CAAC;IAAA,CACf;IAEO,KAAK,CAAC,mBAAmB,CAChC,MAAwB,EACmC;QAC3D,IAAI,CAAC;YACJ,IAAI,OAA2B,CAAC;YAChC,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;gBAC5D,OAAO,GAAG,OAAO,CAAC;gBAClB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;YAAA,CAC7B,CAAC,CAAC;YACH,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QAClG,CAAC;IAAA,CACD;IAEO,KAAK,CAAC,UAAU,GAA6D;QACpF,IAAI,CAAC;YACJ,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YACzD,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;YACvF,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QAClG,CAAC;IAAA,CACD;IAEO,qBAAqB,GAG1B;QACF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;YACvG,MAAM;YACN,IAAI;SACJ,CAAC,CAAC,CAAC;IAAA,CACJ;IAED,KAAK,CAAC,QAAQ,GAAqC;QAClD,wEAAwE;QACxE,uEAAuE;QACvE,2DAA2D;QAC3D,MAAM,CAAC,aAAa,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACzD,IAAI,CAAC,qBAAqB,EAAE;YAC5B,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC;YACrC,IAAI,CAAC,uBAAuB,CAAC,SAAS,CAAC;SACvC,CAAC,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC;QACvC,MAAM,MAAM,GAA2B,EAAE,CAAC;QAC1C,MAAM,UAAU,GAAG,CAClB,MAAwB,EACxB,KAAsD,EACnC,EAAE,CAAC;YACtB,IAAI,KAAK,CAAC,KAAK;gBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;YAC7D,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;QAAA,CAC9D,CAAC;QAEF,MAAM,KAAK,GAAqB;YAC/B,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;YAC/B,GAAG,IAAI,CAAC,YAAY;YACpB,GAAG,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC;YAC7B,GAAG,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC;YACjC,GAAG,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC;YAC3B,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,SAAkB,EAAE,CAAC,CAAC;YACrF,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,SAAkB,EAAE,CAAC,CAAC;SACrF,CAAC;QAEF,MAAM,aAAa,GAAG,IAAI,GAAG,EAA4C,CAAC;QAC1E,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI;YAAE,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnE,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI;YAAE,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChE,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACvF,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAEvF,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;IAAA,CACxC;IAED,KAAK,CAAC,KAAK,CAAC,MAAwB,EAAiB;QACpD,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YAC1E,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAC9C,OAAO;QACR,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,cAAc,IAAI,CAAC,MAAM,CAAC,WAAW,KAAK,OAAO,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,CAAC,EAAE,CAAC;YAClG,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,CAAC,WAAW,mBAAmB,CAAC,CAAC;QACnF,CAAC;QACD,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;QACvC,MAAM,cAAc,GAAG,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACvG,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAClD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,WAAW,CAAC,CAAC;YAChE,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACjD,MAAM,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,aAAa,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;gBAC3D,MAAM,EAAE,SAAS;gBACjB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;aACnC,CAAC,CAAC,CAAC;YACJ,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAAA,CAChE,CAAC,CAAC;QACH,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC;QAChD,MAAM,UAAU,CAAC;IAAA,CACjB;IAEO,YAAY,CAAC,WAA0B,EAAE,MAAwB,EAAQ;QAChF,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;YAC7C,OAAO;QACR,CAAC;QACD,MAAM,OAAO,GAA0B,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,EAAE,CAAC;QACzG,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC;IAAA,CACrE;CACD;AASD;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,0BAA0B;IACrB,MAAM,CAAsB;IAC5B,YAAY,GAAkD,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC3F,YAAY,GAAmD,EAAE,CAAC;IAEnF,YAAY,OAA0C,EAAE;QACvD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAAA,CAC7B;IAED,KAAK,CAAC,QAAQ,GAAqC;QAClD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpD,MAAM,KAAK,GAAqB;YAC/B,GAAG,cAAc,CAAC,KAAK;YACvB,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,SAAkB,EAAE,CAAC,CAAC;YACrF,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,SAAkB,EAAE,CAAC,CAAC;SACrF,CAAC;QACF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,KAAK,SAAS;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACrG,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,KAAK,SAAS;YAAE,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACrG,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,CAAC,MAAM,EAAE,CAAC;IAAA,CAC/D;IAED,KAAK,CAAC,KAAK,CAAC,MAAwB,EAAiB;QACpD,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YAC1E,MAAM,IAAI,KAAK,CACd,yDAAyD,MAAM,CAAC,WAAW,iFAAiF,CAC5J,CAAC;QACH,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;YACpD,OAAO;QACR,CAAC;QACD,MAAM,OAAO,GAA0B;YACtC,OAAO,EAAE,aAAa;YACtB,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC;SAC5C,CAAC;QACF,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC;IAAA,CAC5E;CACD","sourcesContent":["/**\n * Persistence for the permission rule model (ADR 0004). Four of the eight sources\n * are file-backed (policy, local, project, user); the other four are runtime-only\n * (flag, cliArg — parsed once from argv, not stored here; command, session — held\n * in memory for the life of the process). `session`-source updates are never\n * written to disk: \"allow for this session\" dies with the session by design.\n *\n * Reuses the locked-JSON-file backend already proven for Phase 1's operational\n * state (FileAuthStorageBackend) rather than widening `SettingsScope`, which only\n * covers two of these four file-backed sources and represents a different, larger\n * settings domain.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { type AuthStorageBackend, FileAuthStorageBackend } from \"../auth-storage.ts\";\nimport {\n\tPROJECT_LOCAL_PERMISSIONS_FILE,\n\tPROJECT_PERMISSIONS_FILE,\n\tprojectResourcePathByName,\n} from \"../project-resources.ts\";\nimport type { PermissionBehavior } from \"../tools/contract.ts\";\nimport type { PermissionRule, PermissionSource } from \"./rules.ts\";\n\nexport const PERMISSION_MODES = [\"default\", \"plan\", \"acceptEdits\", \"bypassPermissions\", \"dontAsk\"] as const;\nexport type PermissionMode = (typeof PERMISSION_MODES)[number];\n\n/** Sources a PermissionUpdate may target. Excludes `policy` (administrator-managed,\n * never user-writable) and `flag`/`cliArg` (parsed once from argv at startup, not\n * something this store updates after the fact). */\nexport const WRITABLE_PERMISSION_SOURCES = [\"local\", \"project\", \"user\", \"command\", \"session\"] as const;\nexport type WritablePermissionSource = (typeof WRITABLE_PERMISSION_SOURCES)[number];\n\nconst FILE_BACKED_SOURCES = [\"local\", \"project\", \"user\"] as const;\ntype FileBackedSource = (typeof FILE_BACKED_SOURCES)[number];\n\nconst RUNTIME_SOURCES = [\"command\", \"session\"] as const;\ntype RuntimeSource = (typeof RUNTIME_SOURCES)[number];\n\n/** A rule as persisted: `source` is never stored — it is implied by which file it came from. */\nexport type StoredPermissionRule = Omit<PermissionRule, \"source\">;\n\nexport interface RuleMatcher {\n\ttoolName: string;\n\truleContent?: string;\n}\n\nexport type PermissionUpdate =\n\t| { type: \"addRules\"; destination: WritablePermissionSource; rules: readonly StoredPermissionRule[] }\n\t| { type: \"replaceRules\"; destination: WritablePermissionSource; rules: readonly StoredPermissionRule[] }\n\t| { type: \"removeRules\"; destination: WritablePermissionSource; matching: readonly RuleMatcher[] }\n\t| { type: \"setMode\"; destination: WritablePermissionSource; mode: PermissionMode };\n\nexport interface PermissionStoreError {\n\tsource: PermissionSource;\n\terror: Error;\n}\n\nexport interface PermissionStoreSnapshot {\n\trules: readonly PermissionRule[];\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>;\n\terrors: readonly PermissionStoreError[];\n}\n\nexport interface PermissionRuleStore {\n\tsnapshot(): Promise<PermissionStoreSnapshot>;\n\tapply(update: PermissionUpdate): Promise<void>;\n}\n\nconst STORE_VERSION = 1;\n\ninterface StoredPermissionScope {\n\tversion: number;\n\trules: StoredPermissionRule[];\n\tmode?: PermissionMode;\n}\n\nfunction emptyScope(): StoredPermissionScope {\n\treturn { version: STORE_VERSION, rules: [] };\n}\n\n/**\n * Whether a permission file's contents grant anything. The trust classifier asks this\n * so a `{}` file does not raise a prompt that carries no decision. Unparseable content\n * counts as granting: a malformed or hostile file must prompt rather than pass.\n */\nexport function permissionContentConfersAuthority(content: string | undefined): boolean {\n\tlet scope: StoredPermissionScope;\n\ttry {\n\t\tscope = parseScope(content);\n\t} catch {\n\t\treturn true;\n\t}\n\treturn scope.rules.length > 0 || scope.mode !== undefined;\n}\n\nfunction isPermissionBehavior(value: unknown): value is PermissionBehavior {\n\treturn value === \"allow\" || value === \"deny\" || value === \"ask\";\n}\n\nfunction parseScope(content: string | undefined): StoredPermissionScope {\n\tif (!content) return emptyScope();\n\tconst parsed = JSON.parse(content);\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new Error(\"Invalid permission rules file: unexpected shape\");\n\t}\n\t// FileAuthStorageBackend (shared with auth.json) seeds a freshly created file\n\t// with the literal text \"{}\". Treat a content-free object as \"nothing written\n\t// yet\" rather than malformed; a real malformed file has some recognizable but\n\t// wrong-shaped field, caught below.\n\tif (!(\"version\" in parsed) && !(\"rules\" in parsed) && !(\"mode\" in parsed)) {\n\t\treturn emptyScope();\n\t}\n\tif (\n\t\tparsed.version !== STORE_VERSION ||\n\t\t(parsed.mode !== undefined && !(PERMISSION_MODES as readonly string[]).includes(parsed.mode)) ||\n\t\t!Array.isArray(parsed.rules) ||\n\t\t!parsed.rules.every(\n\t\t\t(rule: unknown) =>\n\t\t\t\ttypeof rule === \"object\" &&\n\t\t\t\trule !== null &&\n\t\t\t\ttypeof (rule as StoredPermissionRule).toolName === \"string\" &&\n\t\t\t\tisPermissionBehavior((rule as StoredPermissionRule).behavior) &&\n\t\t\t\t((rule as StoredPermissionRule).ruleContent === undefined ||\n\t\t\t\t\ttypeof (rule as StoredPermissionRule).ruleContent === \"string\"),\n\t\t)\n\t) {\n\t\tthrow new Error(\"Invalid permission rules file: unexpected shape\");\n\t}\n\treturn parsed as StoredPermissionScope;\n}\n\nfunction applyToScope(scope: StoredPermissionScope, update: PermissionUpdate): StoredPermissionScope {\n\tswitch (update.type) {\n\t\tcase \"addRules\":\n\t\t\treturn { ...scope, rules: [...scope.rules, ...update.rules] };\n\t\tcase \"replaceRules\":\n\t\t\treturn { ...scope, rules: [...update.rules] };\n\t\tcase \"removeRules\":\n\t\t\treturn {\n\t\t\t\t...scope,\n\t\t\t\trules: scope.rules.filter(\n\t\t\t\t\t(rule) =>\n\t\t\t\t\t\t!update.matching.some((m) => m.toolName === rule.toolName && m.ruleContent === rule.ruleContent),\n\t\t\t\t),\n\t\t\t};\n\t\tcase \"setMode\":\n\t\t\treturn { ...scope, mode: update.mode };\n\t}\n}\n\nfunction defaultPolicyPath(): string {\n\tif (process.env.APEX_CODE_POLICY_PATH) return process.env.APEX_CODE_POLICY_PATH;\n\treturn process.platform === \"win32\"\n\t\t? join(process.env.ProgramData ?? \"C:\\\\ProgramData\", \"apex-code\", \"policy.json\")\n\t\t: \"/etc/apex-code/policy.json\";\n}\n\nexport interface CreateFilePermissionRuleStoreOptions {\n\tcwd: string;\n\tagentDir?: string;\n\t/** Overrides the managed policy file path. Defaults to $APEX_CODE_POLICY_PATH, else an OS-specific system location. */\n\tpolicyPath?: string;\n\t/** Test seam: inject backends for the three writable file-backed sources instead of touching real files. */\n\tbackends?: Partial<Record<FileBackedSource, AuthStorageBackend>>;\n\t/** Immutable argv layers (`flag` / `cliArg`), validated by the CLI before construction. */\n\tinitialRules?: readonly PermissionRule[];\n\t/**\n\t * Resolved before construction. False excludes both project-controlled scopes.\n\t *\n\t * Required, per ADR 0034. There is no correct default: a caller that has not resolved\n\t * trust cannot be given either answer about an untrusted checkout.\n\t */\n\tprojectTrusted: boolean;\n}\n\n/** File-backed store for policy/local/project/user, in-memory for command/session. */\nexport class FilePermissionRuleStore implements PermissionRuleStore {\n\tprivate readonly backends: Record<FileBackedSource, AuthStorageBackend>;\n\tprivate readonly policyPath: string;\n\tprivate readonly initialRules: readonly PermissionRule[];\n\tprivate readonly runtimeRules: Record<RuntimeSource, StoredPermissionRule[]> = { command: [], session: [] };\n\tprivate readonly runtimeModes: Partial<Record<RuntimeSource, PermissionMode>> = {};\n\tprivate readonly projectTrusted: boolean;\n\tprivate readonly fileScopes: Partial<\n\t\tRecord<FileBackedSource, Promise<{ scope: StoredPermissionScope; error?: Error }>>\n\t> = {};\n\tprivate readonly fileScopeUpdates: Partial<Record<FileBackedSource, Promise<void>>> = {};\n\n\tconstructor(options: CreateFilePermissionRuleStoreOptions) {\n\t\tconst agentDir = options.agentDir ?? getAgentDir();\n\t\tconst cwd = resolvePath(options.cwd);\n\t\tthis.backends = {\n\t\t\tuser: new FileAuthStorageBackend(join(agentDir, \"permissions.json\")),\n\t\t\tproject: new FileAuthStorageBackend(projectResourcePathByName(cwd, PROJECT_PERMISSIONS_FILE)),\n\t\t\tlocal: new FileAuthStorageBackend(projectResourcePathByName(cwd, PROJECT_LOCAL_PERMISSIONS_FILE)),\n\t\t\t...options.backends,\n\t\t};\n\t\tthis.policyPath = options.policyPath ?? defaultPolicyPath();\n\t\tthis.initialRules = options.initialRules ?? [];\n\t\t// A JavaScript caller reaches this with no compiler in between, so the type is not\n\t\t// the whole guard. ADR 0034.\n\t\tif (typeof options.projectTrusted !== \"boolean\") {\n\t\t\tthrow new TypeError(\"FilePermissionRuleStore requires an explicit projectTrusted decision (ADR 0034)\");\n\t\t}\n\t\tthis.projectTrusted = options.projectTrusted;\n\t\tif (!this.projectTrusted) {\n\t\t\tthis.fileScopes.local = Promise.resolve({ scope: emptyScope() });\n\t\t\tthis.fileScopes.project = Promise.resolve({ scope: emptyScope() });\n\t\t}\n\t}\n\n\tprivate capturedFileBackedScope(source: FileBackedSource): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\tconst captured = this.fileScopes[source];\n\t\tif (captured) return captured;\n\t\tconst initial = this.readFileBackedScope(source);\n\t\tthis.fileScopes[source] = initial;\n\t\treturn initial;\n\t}\n\n\tprivate async readFileBackedScope(\n\t\tsource: FileBackedSource,\n\t): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\ttry {\n\t\t\tlet content: string | undefined;\n\t\t\tawait this.backends[source].withLockAsync(async (current) => {\n\t\t\t\tcontent = current;\n\t\t\t\treturn { result: undefined };\n\t\t\t});\n\t\t\treturn { scope: parseScope(content) };\n\t\t} catch (error) {\n\t\t\treturn { scope: emptyScope(), error: error instanceof Error ? error : new Error(String(error)) };\n\t\t}\n\t}\n\n\tprivate async readPolicy(): Promise<{ scope: StoredPermissionScope; error?: Error }> {\n\t\ttry {\n\t\t\tconst content = await readFile(this.policyPath, \"utf-8\");\n\t\t\treturn { scope: parseScope(content) };\n\t\t} catch (error) {\n\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return { scope: emptyScope() };\n\t\t\treturn { scope: emptyScope(), error: error instanceof Error ? error : new Error(String(error)) };\n\t\t}\n\t}\n\n\tprivate capturedPolicyAndUser(): Promise<{\n\t\tpolicy: { scope: StoredPermissionScope; error?: Error };\n\t\tuser: { scope: StoredPermissionScope; error?: Error };\n\t}> {\n\t\treturn Promise.all([this.readPolicy(), this.capturedFileBackedScope(\"user\")]).then(([policy, user]) => ({\n\t\t\tpolicy,\n\t\t\tuser,\n\t\t}));\n\t}\n\n\tasync snapshot(): Promise<PermissionStoreSnapshot> {\n\t\t// File-backed authorization is captured once. Direct filesystem writes,\n\t\t// including symlink replacement, cannot widen the current or next gate\n\t\t// decision. Managed policy and runtime scopes remain live.\n\t\tconst [policyAndUser, local, project] = await Promise.all([\n\t\t\tthis.capturedPolicyAndUser(),\n\t\t\tthis.capturedFileBackedScope(\"local\"),\n\t\t\tthis.capturedFileBackedScope(\"project\"),\n\t\t]);\n\t\tconst { policy, user } = policyAndUser;\n\t\tconst errors: PermissionStoreError[] = [];\n\t\tconst withSource = (\n\t\t\tsource: PermissionSource,\n\t\t\tentry: { scope: StoredPermissionScope; error?: Error },\n\t\t): PermissionRule[] => {\n\t\t\tif (entry.error) errors.push({ source, error: entry.error });\n\t\t\treturn entry.scope.rules.map((rule) => ({ ...rule, source }));\n\t\t};\n\n\t\tconst rules: PermissionRule[] = [\n\t\t\t...withSource(\"policy\", policy),\n\t\t\t...this.initialRules,\n\t\t\t...withSource(\"local\", local),\n\t\t\t...withSource(\"project\", project),\n\t\t\t...withSource(\"user\", user),\n\t\t\t...this.runtimeRules.command.map((rule) => ({ ...rule, source: \"command\" as const })),\n\t\t\t...this.runtimeRules.session.map((rule) => ({ ...rule, source: \"session\" as const })),\n\t\t];\n\n\t\tconst modesBySource = new Map<WritablePermissionSource, PermissionMode>();\n\t\tif (local.scope.mode) modesBySource.set(\"local\", local.scope.mode);\n\t\tif (project.scope.mode) modesBySource.set(\"project\", project.scope.mode);\n\t\tif (user.scope.mode) modesBySource.set(\"user\", user.scope.mode);\n\t\tif (this.runtimeModes.command) modesBySource.set(\"command\", this.runtimeModes.command);\n\t\tif (this.runtimeModes.session) modesBySource.set(\"session\", this.runtimeModes.session);\n\n\t\treturn { rules, modesBySource, errors };\n\t}\n\n\tasync apply(update: PermissionUpdate): Promise<void> {\n\t\tif (update.destination === \"command\" || update.destination === \"session\") {\n\t\t\tthis.applyRuntime(update.destination, update);\n\t\t\treturn;\n\t\t}\n\t\tif (!this.projectTrusted && (update.destination === \"local\" || update.destination === \"project\")) {\n\t\t\tthrow new Error(`Cannot update untrusted ${update.destination} permission scope`);\n\t\t}\n\t\tconst destination = update.destination;\n\t\tconst previousUpdate = this.fileScopeUpdates[destination]?.catch(() => undefined) ?? Promise.resolve();\n\t\tconst nextUpdate = previousUpdate.then(async () => {\n\t\t\tconst current = await this.capturedFileBackedScope(destination);\n\t\t\tconst next = applyToScope(current.scope, update);\n\t\t\tawait this.backends[destination].withLockAsync(async () => ({\n\t\t\t\tresult: undefined,\n\t\t\t\tnext: JSON.stringify(next, null, 2),\n\t\t\t}));\n\t\t\tthis.fileScopes[destination] = Promise.resolve({ scope: next });\n\t\t});\n\t\tthis.fileScopeUpdates[destination] = nextUpdate;\n\t\tawait nextUpdate;\n\t}\n\n\tprivate applyRuntime(destination: RuntimeSource, update: PermissionUpdate): void {\n\t\tif (update.type === \"setMode\") {\n\t\t\tthis.runtimeModes[destination] = update.mode;\n\t\t\treturn;\n\t\t}\n\t\tconst current: StoredPermissionScope = { version: STORE_VERSION, rules: this.runtimeRules[destination] };\n\t\tthis.runtimeRules[destination] = applyToScope(current, update).rules;\n\t}\n}\n\nexport interface DerivedPermissionRuleStoreOptions {\n\t/** The parent's live store. Read fresh on every `snapshot()`, never cached, so a\n\t * rule the parent picks up mid-session (e.g. a human's \"always allow\") is visible\n\t * to the child on its very next call. */\n\tparent: PermissionRuleStore;\n}\n\n/**\n * A delegated child's permission store (roadmap Phase 5, ADR 0008). A read-through\n * view over the parent's live snapshot -- including its runtime-only sources\n * (`flag`, `cliArg`, `command`, `session`), which a subprocess-spawned child could\n * never observe -- plus a child-local runtime overlay for the child's own writes.\n *\n * `apply()` never reaches the parent. It writes only into this instance's own\n * overlay, and only for the two runtime-only writable sources (`command`, `session`);\n * a file-backed destination (`local`, `project`, `user`) is rejected outright, not\n * silently redirected -- a delegated child must never be able to persist to the\n * parent's settings files, and \"runtime-only\" is enforced by construction here, not\n * by a convention a future caller could forget. The overlay is discarded with the\n * child: there is no code path from a child's approval back into the parent's store,\n * which is what keeps a child's `persist: true` from widening its parent.\n */\nexport class DerivedPermissionRuleStore implements PermissionRuleStore {\n\tprivate readonly parent: PermissionRuleStore;\n\tprivate readonly overlayRules: Record<RuntimeSource, StoredPermissionRule[]> = { command: [], session: [] };\n\tprivate readonly overlayModes: Partial<Record<RuntimeSource, PermissionMode>> = {};\n\n\tconstructor(options: DerivedPermissionRuleStoreOptions) {\n\t\tthis.parent = options.parent;\n\t}\n\n\tasync snapshot(): Promise<PermissionStoreSnapshot> {\n\t\tconst parentSnapshot = await this.parent.snapshot();\n\t\tconst rules: PermissionRule[] = [\n\t\t\t...parentSnapshot.rules,\n\t\t\t...this.overlayRules.command.map((rule) => ({ ...rule, source: \"command\" as const })),\n\t\t\t...this.overlayRules.session.map((rule) => ({ ...rule, source: \"session\" as const })),\n\t\t];\n\t\tconst modesBySource = new Map(parentSnapshot.modesBySource);\n\t\tif (this.overlayModes.command !== undefined) modesBySource.set(\"command\", this.overlayModes.command);\n\t\tif (this.overlayModes.session !== undefined) modesBySource.set(\"session\", this.overlayModes.session);\n\t\treturn { rules, modesBySource, errors: parentSnapshot.errors };\n\t}\n\n\tasync apply(update: PermissionUpdate): Promise<void> {\n\t\tif (update.destination !== \"command\" && update.destination !== \"session\") {\n\t\t\tthrow new Error(\n\t\t\t\t`A delegated child cannot persist permission rules to \"${update.destination}\" -- only \"command\" and \"session\" are writable from a derived store (ADR 0008).`,\n\t\t\t);\n\t\t}\n\t\tif (update.type === \"setMode\") {\n\t\t\tthis.overlayModes[update.destination] = update.mode;\n\t\t\treturn;\n\t\t}\n\t\tconst current: StoredPermissionScope = {\n\t\t\tversion: STORE_VERSION,\n\t\t\trules: this.overlayRules[update.destination],\n\t\t};\n\t\tthis.overlayRules[update.destination] = applyToScope(current, update).rules;\n\t}\n}\n"]}
|
|
@@ -47,6 +47,18 @@ export declare const PROJECT_PERMISSIONS_FILE = "permissions.json";
|
|
|
47
47
|
export declare const PROJECT_LOCAL_PERMISSIONS_FILE = "permissions.local.json";
|
|
48
48
|
export declare const PROJECT_AGENTS_DIR = "agents";
|
|
49
49
|
export declare const PROJECT_MCP_CONFIG_FILE = ".mcp.json";
|
|
50
|
+
/**
|
|
51
|
+
* The repository instruction files, in the order the loader prefers them.
|
|
52
|
+
*
|
|
53
|
+
* These are wrapped as `<project_instructions>` and handed to the model as instructions,
|
|
54
|
+
* so a checkout supplying one is steering the agent. That is what the trust prompt asks
|
|
55
|
+
* about, and until this list was registered they reached the system prompt through no gate.
|
|
56
|
+
*
|
|
57
|
+
* The loader reads this array rather than its own copy. A sixth spelling added to one and
|
|
58
|
+
* not the other is the exact divergence this registry exists to prevent, and the earlier
|
|
59
|
+
* form of that bug is why the registry exists at all.
|
|
60
|
+
*/
|
|
61
|
+
export declare const PROJECT_INSTRUCTION_FILES: readonly ["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
|
|
50
62
|
export declare const PROJECT_RESOURCES: readonly ProjectResource[];
|
|
51
63
|
/** The project config directory for `cwd`. Honors a `piConfig.configDir` override. */
|
|
52
64
|
export declare function projectConfigDir(cwd: string): string;
|