apex-code 0.0.1-alpha.8 → 0.0.4
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 +231 -0
- package/README.md +9 -8
- package/dist/cli/args.d.ts +26 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +72 -10
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +2 -1
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/mcp-command.d.ts +9 -0
- package/dist/cli/mcp-command.d.ts.map +1 -0
- package/dist/cli/mcp-command.js +53 -0
- package/dist/cli/mcp-command.js.map +1 -0
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +3 -1
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +2 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +94 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -18
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +183 -15
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +728 -117
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +6 -7
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +7 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
- package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/git-checkpoints.js +251 -0
- package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.js +36 -0
- package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +7 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +9 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +59 -36
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/defaults.d.ts +1 -0
- package/dist/core/defaults.d.ts.map +1 -1
- package/dist/core/defaults.js +9 -0
- package/dist/core/defaults.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +6 -5
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +4 -3
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/durable-state/sqlite.d.ts.map +1 -1
- package/dist/core/durable-state/sqlite.js +26 -2
- package/dist/core/durable-state/sqlite.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +5 -2
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/index.d.ts +2 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +123 -50
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +45 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +14 -0
- package/dist/core/extensions/source-runtime.d.ts.map +1 -0
- package/dist/core/extensions/source-runtime.js +56 -0
- package/dist/core/extensions/source-runtime.js.map +1 -0
- package/dist/core/extensions/types.d.ts +60 -11
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +3 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/formatter-lifecycle.d.ts +69 -0
- package/dist/core/formatter-lifecycle.d.ts.map +1 -0
- package/dist/core/formatter-lifecycle.js +360 -0
- package/dist/core/formatter-lifecycle.js.map +1 -0
- package/dist/core/hooks/command-handler.d.ts +19 -0
- package/dist/core/hooks/command-handler.d.ts.map +1 -0
- package/dist/core/hooks/command-handler.js +112 -0
- package/dist/core/hooks/command-handler.js.map +1 -0
- package/dist/core/hooks/http-handler.d.ts +10 -0
- package/dist/core/hooks/http-handler.d.ts.map +1 -0
- package/dist/core/hooks/http-handler.js +36 -0
- package/dist/core/hooks/http-handler.js.map +1 -0
- package/dist/core/hooks/loader.d.ts +29 -0
- package/dist/core/hooks/loader.d.ts.map +1 -0
- package/dist/core/hooks/loader.js +139 -0
- package/dist/core/hooks/loader.js.map +1 -0
- package/dist/core/hooks/runtime.d.ts +13 -0
- package/dist/core/hooks/runtime.d.ts.map +1 -0
- package/dist/core/hooks/runtime.js +63 -0
- package/dist/core/hooks/runtime.js.map +1 -0
- package/dist/core/hooks/types.d.ts +83 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +24 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/keybindings.d.ts +20 -19
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +26 -5
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mcp/config.d.ts +16 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +166 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/connector.d.ts +35 -0
- package/dist/core/mcp/connector.d.ts.map +1 -0
- package/dist/core/mcp/connector.js +105 -0
- package/dist/core/mcp/connector.js.map +1 -0
- package/dist/core/mcp/contract.d.ts +19 -0
- package/dist/core/mcp/contract.d.ts.map +1 -0
- package/dist/core/mcp/contract.js +89 -0
- package/dist/core/mcp/contract.js.map +1 -0
- package/dist/core/mcp/mcp-tool.d.ts +32 -0
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
- package/dist/core/mcp/mcp-tool.js +171 -0
- package/dist/core/mcp/mcp-tool.js.map +1 -0
- package/dist/core/mcp/metadata-cache.d.ts +29 -0
- package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
- package/dist/core/mcp/metadata-cache.js +99 -0
- package/dist/core/mcp/metadata-cache.js.map +1 -0
- package/dist/core/mcp/oauth/authorize.d.ts +21 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
- package/dist/core/mcp/oauth/authorize.js +32 -0
- package/dist/core/mcp/oauth/authorize.js.map +1 -0
- package/dist/core/mcp/oauth/discover.d.ts +27 -0
- package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
- package/dist/core/mcp/oauth/discover.js +116 -0
- package/dist/core/mcp/oauth/discover.js.map +1 -0
- package/dist/core/mcp/oauth/flow.d.ts +31 -0
- package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
- package/dist/core/mcp/oauth/flow.js +175 -0
- package/dist/core/mcp/oauth/flow.js.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.js +129 -0
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
- package/dist/core/mcp/oauth/pkce.d.ts +12 -0
- package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
- package/dist/core/mcp/oauth/pkce.js +11 -0
- package/dist/core/mcp/oauth/pkce.js.map +1 -0
- package/dist/core/mcp/oauth/register.d.ts +16 -0
- package/dist/core/mcp/oauth/register.d.ts.map +1 -0
- package/dist/core/mcp/oauth/register.js +42 -0
- package/dist/core/mcp/oauth/register.js.map +1 -0
- package/dist/core/mcp/oauth/token-client.d.ts +33 -0
- package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
- package/dist/core/mcp/oauth/token-client.js +65 -0
- package/dist/core/mcp/oauth/token-client.js.map +1 -0
- package/dist/core/mcp/runtime.d.ts +20 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -0
- package/dist/core/mcp/runtime.js +37 -0
- package/dist/core/mcp/runtime.js.map +1 -0
- package/dist/core/mcp/schema.d.ts +22 -0
- package/dist/core/mcp/schema.d.ts.map +1 -0
- package/dist/core/mcp/schema.js +20 -0
- package/dist/core/mcp/schema.js.map +1 -0
- package/dist/core/mcp/server-manager.d.ts +43 -0
- package/dist/core/mcp/server-manager.d.ts.map +1 -0
- package/dist/core/mcp/server-manager.js +142 -0
- package/dist/core/mcp/server-manager.js.map +1 -0
- package/dist/core/mcp/types.d.ts +100 -0
- package/dist/core/mcp/types.d.ts.map +1 -0
- package/dist/core/mcp/types.js +8 -0
- package/dist/core/mcp/types.js.map +1 -0
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +3 -1
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +14 -8
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/models-store.d.ts.map +1 -1
- package/dist/core/models-store.js +2 -1
- package/dist/core/models-store.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +22 -14
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/permissions/gate.d.ts.map +1 -1
- package/dist/core/permissions/gate.js +45 -4
- package/dist/core/permissions/gate.js.map +1 -1
- package/dist/core/permissions/operations.d.ts +20 -0
- package/dist/core/permissions/operations.d.ts.map +1 -0
- package/dist/core/permissions/operations.js +9 -0
- package/dist/core/permissions/operations.js.map +1 -0
- package/dist/core/permissions/policy-command.d.ts +42 -0
- package/dist/core/permissions/policy-command.d.ts.map +1 -0
- package/dist/core/permissions/policy-command.js +44 -0
- package/dist/core/permissions/policy-command.js.map +1 -0
- package/dist/core/permissions/responder.d.ts +49 -2
- package/dist/core/permissions/responder.d.ts.map +1 -1
- package/dist/core/permissions/responder.js +20 -8
- package/dist/core/permissions/responder.js.map +1 -1
- package/dist/core/permissions/rules.d.ts.map +1 -1
- package/dist/core/permissions/rules.js +11 -1
- package/dist/core/permissions/rules.js.map +1 -1
- package/dist/core/permissions/store.d.ts +18 -0
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +89 -8
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/pi-manifest.d.ts.map +1 -1
- package/dist/core/pi-manifest.js +2 -1
- package/dist/core/pi-manifest.js.map +1 -1
- package/dist/core/policy-executor.d.ts +47 -0
- package/dist/core/policy-executor.d.ts.map +1 -0
- package/dist/core/policy-executor.js +228 -0
- package/dist/core/policy-executor.js.map +1 -0
- package/dist/core/policy-loader.d.ts +72 -0
- package/dist/core/policy-loader.d.ts.map +1 -0
- package/dist/core/policy-loader.js +195 -0
- package/dist/core/policy-loader.js.map +1 -0
- package/dist/core/project-trust.d.ts +1 -1
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +2 -1
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +3 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
- package/dist/core/sandbox/bwrap-arguments.js +136 -0
- package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
- package/dist/core/sandbox/child-entry.d.ts.map +1 -1
- package/dist/core/sandbox/child-entry.js +3 -1
- package/dist/core/sandbox/child-entry.js.map +1 -1
- package/dist/core/sandbox/cli-launch.d.ts +84 -4
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
- package/dist/core/sandbox/cli-launch.js +115 -16
- package/dist/core/sandbox/cli-launch.js.map +1 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/cli-supervisor.js +50 -2
- package/dist/core/sandbox/cli-supervisor.js.map +1 -1
- package/dist/core/sandbox/full-access.d.ts +28 -0
- package/dist/core/sandbox/full-access.d.ts.map +1 -0
- package/dist/core/sandbox/full-access.js +63 -0
- package/dist/core/sandbox/full-access.js.map +1 -0
- package/dist/core/sandbox/git-identity.d.ts +57 -0
- package/dist/core/sandbox/git-identity.d.ts.map +1 -0
- package/dist/core/sandbox/git-identity.js +78 -0
- package/dist/core/sandbox/git-identity.js.map +1 -0
- package/dist/core/sandbox/host-approval.d.ts +42 -0
- package/dist/core/sandbox/host-approval.d.ts.map +1 -0
- package/dist/core/sandbox/host-approval.js +99 -0
- package/dist/core/sandbox/host-approval.js.map +1 -0
- package/dist/core/sandbox/linux-backend.d.ts +14 -0
- package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
- package/dist/core/sandbox/linux-backend.js +158 -80
- package/dist/core/sandbox/linux-backend.js.map +1 -1
- package/dist/core/sandbox/macos-backend.d.ts +5 -0
- package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
- package/dist/core/sandbox/macos-backend.js +164 -5
- package/dist/core/sandbox/macos-backend.js.map +1 -1
- package/dist/core/sandbox/network-proxy.d.ts +19 -0
- package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/network-proxy.js +93 -19
- package/dist/core/sandbox/network-proxy.js.map +1 -1
- package/dist/core/sandbox/policy.d.ts +8 -0
- package/dist/core/sandbox/policy.d.ts.map +1 -1
- package/dist/core/sandbox/policy.js +29 -12
- package/dist/core/sandbox/policy.js.map +1 -1
- package/dist/core/sandbox/profiles.d.ts +25 -0
- package/dist/core/sandbox/profiles.d.ts.map +1 -0
- package/dist/core/sandbox/profiles.js +23 -0
- package/dist/core/sandbox/profiles.js.map +1 -0
- package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
- package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-client.js +86 -0
- package/dist/core/sandbox/rpc/command-client.js.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.js +120 -0
- package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
- package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
- package/dist/core/sandbox/rpc/framing.d.ts +35 -0
- package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/framing.js +114 -0
- package/dist/core/sandbox/rpc/framing.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
- package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
- package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
- package/dist/core/sandbox/supervisor-temp.js +21 -0
- package/dist/core/sandbox/supervisor-temp.js.map +1 -0
- package/dist/core/sandbox/supervisor.d.ts +12 -1
- package/dist/core/sandbox/supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/supervisor.js +15 -1
- package/dist/core/sandbox/supervisor.js.map +1 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
- package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
- package/dist/core/sandbox/terminal-handoff.js +221 -0
- package/dist/core/sandbox/terminal-handoff.js.map +1 -0
- package/dist/core/sandbox/terminal-size.d.ts +13 -0
- package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
- package/dist/core/sandbox/terminal-size.js +23 -2
- package/dist/core/sandbox/terminal-size.js.map +1 -1
- package/dist/core/sdk.d.ts +24 -2
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +63 -4
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-export.d.ts +4 -0
- package/dist/core/session-export.d.ts.map +1 -0
- package/dist/core/session-export.js +32 -0
- package/dist/core/session-export.js.map +1 -0
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +34 -6
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-diagnostics.d.ts +9 -0
- package/dist/core/settings-diagnostics.d.ts.map +1 -0
- package/dist/core/settings-diagnostics.js +21 -0
- package/dist/core/settings-diagnostics.js.map +1 -0
- package/dist/core/settings-manager.d.ts +172 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +160 -11
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +51 -33
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +8 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/background-shell.d.ts +39 -0
- package/dist/core/tools/background-shell.d.ts.map +1 -0
- package/dist/core/tools/background-shell.js +81 -0
- package/dist/core/tools/background-shell.js.map +1 -0
- package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
- package/dist/core/tools/bash-command-segments.js +2 -0
- package/dist/core/tools/bash-command-segments.js.map +1 -1
- package/dist/core/tools/bash.d.ts +44 -16
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +264 -32
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/contract-snapshot.d.ts +58 -0
- package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
- package/dist/core/tools/contract-snapshot.js +51 -0
- package/dist/core/tools/contract-snapshot.js.map +1 -0
- package/dist/core/tools/contract.d.ts +53 -5
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js +22 -2
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +6 -4
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +21 -11
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +48 -5
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +217 -8
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +8 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +72 -10
- 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 +5 -2
- 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 +5 -2
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +14 -2
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +12 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +5 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/lsp.d.ts +19 -1
- package/dist/core/tools/lsp.d.ts.map +1 -1
- package/dist/core/tools/lsp.js +163 -10
- package/dist/core/tools/lsp.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts +3 -0
- package/dist/core/tools/path-permission.d.ts.map +1 -1
- package/dist/core/tools/path-permission.js +16 -4
- package/dist/core/tools/path-permission.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +27 -2
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +221 -4
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/powershell.d.ts +16 -0
- package/dist/core/tools/powershell.d.ts.map +1 -0
- package/dist/core/tools/powershell.js +39 -0
- package/dist/core/tools/powershell.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +20 -11
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/test.d.ts +54 -9
- package/dist/core/tools/test.d.ts.map +1 -1
- package/dist/core/tools/test.js +239 -19
- package/dist/core/tools/test.js.map +1 -1
- package/dist/core/tools/write.d.ts +7 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +64 -9
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/trust-manager.d.ts.map +1 -1
- package/dist/core/trust-manager.js +2 -1
- package/dist/core/trust-manager.js.map +1 -1
- package/dist/core/verification-lifecycle.d.ts +77 -0
- package/dist/core/verification-lifecycle.d.ts.map +1 -0
- package/dist/core/verification-lifecycle.js +146 -0
- package/dist/core/verification-lifecycle.js.map +1 -0
- package/dist/core/workspace/artifacts.d.ts +45 -0
- package/dist/core/workspace/artifacts.d.ts.map +1 -0
- package/dist/core/workspace/artifacts.js +165 -0
- package/dist/core/workspace/artifacts.js.map +1 -0
- package/dist/core/workspace/comparison.d.ts +27 -0
- package/dist/core/workspace/comparison.d.ts.map +1 -0
- package/dist/core/workspace/comparison.js +94 -0
- package/dist/core/workspace/comparison.js.map +1 -0
- package/dist/core/workspace/git-observer.d.ts +48 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -0
- package/dist/core/workspace/git-observer.js +346 -0
- package/dist/core/workspace/git-observer.js.map +1 -0
- package/dist/core/workspace/projection.d.ts +3 -0
- package/dist/core/workspace/projection.d.ts.map +1 -0
- package/dist/core/workspace/projection.js +56 -0
- package/dist/core/workspace/projection.js.map +1 -0
- package/dist/core/workspace/state.d.ts +133 -0
- package/dist/core/workspace/state.d.ts.map +1 -0
- package/dist/core/workspace/state.js +96 -0
- package/dist/core/workspace/state.js.map +1 -0
- package/dist/extensions/llama/client.d.ts +6 -0
- package/dist/extensions/llama/client.d.ts.map +1 -1
- package/dist/extensions/llama/client.js +7 -0
- package/dist/extensions/llama/client.js.map +1 -1
- package/dist/extensions/llama/index.d.ts.map +1 -1
- package/dist/extensions/llama/index.js +2 -0
- package/dist/extensions/llama/index.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts +3 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +29 -4
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +6 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +58 -23
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +4 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/acp/server.d.ts +55 -0
- package/dist/modes/acp/server.d.ts.map +1 -0
- package/dist/modes/acp/server.js +244 -0
- package/dist/modes/acp/server.js.map +1 -0
- package/dist/modes/acp/translate.d.ts +37 -0
- package/dist/modes/acp/translate.d.ts.map +1 -0
- package/dist/modes/acp/translate.js +50 -0
- package/dist/modes/acp/translate.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +182 -23
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +20 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +20 -9
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +7 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +19 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +70 -10
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +10 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +56 -14
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
- package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
- package/dist/modes/interactive/components/permission-preview.js +33 -0
- package/dist/modes/interactive/components/permission-preview.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +9 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +128 -56
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-submenu.d.ts +72 -0
- package/dist/modes/interactive/components/settings-submenu.d.ts.map +1 -0
- package/dist/modes/interactive/components/settings-submenu.js +165 -0
- package/dist/modes/interactive/components/settings-submenu.js.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +74 -17
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +64 -10
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts.map +1 -1
- package/dist/modes/interactive/external-editor.js +2 -1
- package/dist/modes/interactive/external-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +45 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +395 -43
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +20 -9
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/json-event.d.ts +16 -13
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +20 -6
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +27 -12
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +12 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts +1 -0
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +196 -10
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +2 -1
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/management-http.d.ts +5 -3
- package/dist/utils/management-http.d.ts.map +1 -1
- package/dist/utils/management-http.js +14 -10
- package/dist/utils/management-http.js.map +1 -1
- package/dist/utils/shell.d.ts +3 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +23 -9
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts +1 -0
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +60 -1
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/dist/utils/text.d.ts +8 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +9 -0
- package/dist/utils/text.js.map +1 -0
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +6 -4
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +19 -0
- package/docs/custom-provider.md +12 -9
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +35 -7
- package/docs/json.md +7 -2
- package/docs/keybindings.md +9 -9
- package/docs/llama-cpp.md +2 -0
- package/docs/models.md +8 -2
- package/docs/packages.md +2 -2
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +32 -3
- package/docs/sdk.md +32 -2
- package/docs/settings.md +23 -7
- package/docs/skills.md +4 -3
- package/docs/terminal-setup.md +23 -7
- package/docs/usage.md +10 -5
- package/docs/windows.md +24 -2
- package/examples/extensions/border-status-editor.ts +1 -1
- package/examples/extensions/built-in-tool-renderer.ts +8 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-checkpoint.ts +24 -36
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +2 -2
- package/examples/extensions/notify.ts +3 -1
- package/examples/extensions/permission-gate.ts +1 -1
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/rpc-demo.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +1 -1
- package/examples/extensions/subagent/index.ts +6 -1
- package/examples/extensions/titlebar-spinner.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/rpc-extension-ui.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/npm-shrinkwrap.json +439 -405
- package/package.json +17 -17
- package/dist/server/create-harness.d.ts +0 -26
- package/dist/server/create-harness.d.ts.map +0 -1
- package/dist/server/create-harness.js +0 -107
- package/dist/server/create-harness.js.map +0 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VF.5 plus PS.2: the formatter lifecycle. A formatter runs against an
|
|
3
|
+
* isolated copy of the workspace, never the workspace itself, and only the
|
|
4
|
+
* changes matching its declared paths are promoted back. An undeclared write
|
|
5
|
+
* is left behind in the discarded stage directory, so the live workspace never
|
|
6
|
+
* holds bytes the policy did not declare.
|
|
7
|
+
*
|
|
8
|
+
* This replaces post-hoc reporting. The earlier lifecycle ran the formatter in
|
|
9
|
+
* the live workspace and listed undeclared writes afterwards, which is a
|
|
10
|
+
* report, not a boundary: the stray bytes were already on disk. `status` is
|
|
11
|
+
* now `scope-violated` whenever a write fell outside the declared set, and
|
|
12
|
+
* that value can never read as `passed`.
|
|
13
|
+
*
|
|
14
|
+
* What this does NOT confine, deliberately and recorded rather than implied:
|
|
15
|
+
* a formatter writing to an absolute path outside the workspace, or reaching
|
|
16
|
+
* the network. Those are the OS sandbox's job. Copy plus restricted promotion
|
|
17
|
+
* confines workspace mutation, nothing wider.
|
|
18
|
+
*
|
|
19
|
+
* Promotion refuses any file whose live bytes changed during the run, because
|
|
20
|
+
* overwriting there would destroy a concurrent edit by the user. Such a run
|
|
21
|
+
* fails rather than reverting the user's work.
|
|
22
|
+
*
|
|
23
|
+
* The snapshot is a bounded walk (harness directories skipped, per-file and
|
|
24
|
+
* total byte caps). `truncatedSnapshot` is set when coverage was cut short, so
|
|
25
|
+
* an "unchanged" verdict can be read with the right suspicion.
|
|
26
|
+
*/
|
|
27
|
+
import type { AuthorizeConfiguredCommand } from "./permissions/policy-command.ts";
|
|
28
|
+
import { type PolicyRunStatus } from "./policy-executor.ts";
|
|
29
|
+
import type { FormatterPolicy } from "./policy-loader.ts";
|
|
30
|
+
import type { WorkspaceArtifactRef, WorkspaceArtifactStore } from "./workspace/artifacts.ts";
|
|
31
|
+
export interface FormatterMutationReport {
|
|
32
|
+
/** Workspace-relative paths (forward slashes) that differ before vs after. */
|
|
33
|
+
changedPaths: string[];
|
|
34
|
+
/** Changed paths the policy did not declare inside its pathScope. */
|
|
35
|
+
undeclaredPaths: string[];
|
|
36
|
+
/** Changed paths whose real location resolves outside the workspace. */
|
|
37
|
+
escapedPaths: string[];
|
|
38
|
+
/** True when nothing in the snapshot differs. */
|
|
39
|
+
unchanged: boolean;
|
|
40
|
+
/** True when the walk hit its caps before covering the whole workspace. */
|
|
41
|
+
truncatedSnapshot: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface FormatterEvidence {
|
|
44
|
+
policyId: string;
|
|
45
|
+
executable: string;
|
|
46
|
+
argv: string[];
|
|
47
|
+
cwd: string;
|
|
48
|
+
status: PolicyRunStatus;
|
|
49
|
+
durationMs: number;
|
|
50
|
+
exitCode?: number;
|
|
51
|
+
signal?: string;
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
artifact?: WorkspaceArtifactRef;
|
|
54
|
+
}
|
|
55
|
+
export interface FormatterRunOutcome {
|
|
56
|
+
status: PolicyRunStatus;
|
|
57
|
+
mutations: FormatterMutationReport;
|
|
58
|
+
evidence: FormatterEvidence;
|
|
59
|
+
refusalReason?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface FormatterRunOptions {
|
|
62
|
+
workspaceRoot: string;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
artifactStore?: WorkspaceArtifactStore;
|
|
65
|
+
/** PS.1. A blocked formatter is refused before the snapshot and before the spawn. */
|
|
66
|
+
authorize?: AuthorizeConfiguredCommand;
|
|
67
|
+
}
|
|
68
|
+
export declare function runFormatterCommand(policy: FormatterPolicy, options: FormatterRunOptions): Promise<FormatterRunOutcome>;
|
|
69
|
+
//# sourceMappingURL=formatter-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter-lifecycle.d.ts","sourceRoot":"","sources":["../../src/core/formatter-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAiBH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAClF,OAAO,EAAE,KAAK,eAAe,EAAoB,MAAM,sBAAsB,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAa7F,MAAM,WAAW,uBAAuB;IACvC,8EAA8E;IAC9E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,qEAAqE;IACrE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,wEAAwE;IACxE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,iDAAiD;IACjD,SAAS,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,iBAAiB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,eAAe,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,SAAS,EAAE,uBAAuB,CAAC;IACnC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,qFAAqF;IACrF,SAAS,CAAC,EAAE,0BAA0B,CAAC;CACvC;AAyPD,wBAAsB,mBAAmB,CACxC,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,mBAAmB,GAC1B,OAAO,CAAC,mBAAmB,CAAC,CAkG9B","sourcesContent":["/**\n * VF.5 plus PS.2: the formatter lifecycle. A formatter runs against an\n * isolated copy of the workspace, never the workspace itself, and only the\n * changes matching its declared paths are promoted back. An undeclared write\n * is left behind in the discarded stage directory, so the live workspace never\n * holds bytes the policy did not declare.\n *\n * This replaces post-hoc reporting. The earlier lifecycle ran the formatter in\n * the live workspace and listed undeclared writes afterwards, which is a\n * report, not a boundary: the stray bytes were already on disk. `status` is\n * now `scope-violated` whenever a write fell outside the declared set, and\n * that value can never read as `passed`.\n *\n * What this does NOT confine, deliberately and recorded rather than implied:\n * a formatter writing to an absolute path outside the workspace, or reaching\n * the network. Those are the OS sandbox's job. Copy plus restricted promotion\n * confines workspace mutation, nothing wider.\n *\n * Promotion refuses any file whose live bytes changed during the run, because\n * overwriting there would destroy a concurrent edit by the user. Such a run\n * fails rather than reverting the user's work.\n *\n * The snapshot is a bounded walk (harness directories skipped, per-file and\n * total byte caps). `truncatedSnapshot` is set when coverage was cut short, so\n * an \"unchanged\" verdict can be read with the right suspicion.\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n\tcopyFileSync,\n\ttype Dirent,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\trealpathSync,\n\trmSync,\n\tstatSync,\n} from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { minimatch } from \"minimatch\";\nimport type { AuthorizeConfiguredCommand } from \"./permissions/policy-command.ts\";\nimport { type PolicyRunStatus, runPolicyCommand } from \"./policy-executor.ts\";\nimport type { FormatterPolicy } from \"./policy-loader.ts\";\nimport { preparePathOperation, writePreparedPath } from \"./tools/path-utils.ts\";\nimport type { WorkspaceArtifactRef, WorkspaceArtifactStore } from \"./workspace/artifacts.ts\";\n\n/** Per-file hash cap: larger files compare by size plus prefix hash. */\nconst MAX_HASH_BYTES_PER_FILE = 5 * 1024 * 1024;\n/** Prefix length hashed when a file exceeds the per-file cap. */\nconst LARGE_FILE_PREFIX_BYTES = 64 * 1024;\n/** File-count cap for the before/after walk. */\nconst MAX_SNAPSHOT_FILES = 2_000;\n/** Total bytes hashed across one snapshot. */\nconst MAX_SNAPSHOT_BYTES = 32 * 1024 * 1024;\n/** Directory names never part of a formatter's honest workspace view. */\nconst SKIPPED_DIRECTORIES = new Set([\".git\", \"node_modules\", \".apex-code\", \"sessions\"]);\n\nexport interface FormatterMutationReport {\n\t/** Workspace-relative paths (forward slashes) that differ before vs after. */\n\tchangedPaths: string[];\n\t/** Changed paths the policy did not declare inside its pathScope. */\n\tundeclaredPaths: string[];\n\t/** Changed paths whose real location resolves outside the workspace. */\n\tescapedPaths: string[];\n\t/** True when nothing in the snapshot differs. */\n\tunchanged: boolean;\n\t/** True when the walk hit its caps before covering the whole workspace. */\n\ttruncatedSnapshot: boolean;\n}\n\nexport interface FormatterEvidence {\n\tpolicyId: string;\n\texecutable: string;\n\targv: string[];\n\tcwd: string;\n\tstatus: PolicyRunStatus;\n\tdurationMs: number;\n\texitCode?: number;\n\tsignal?: string;\n\ttruncated: boolean;\n\tartifact?: WorkspaceArtifactRef;\n}\n\nexport interface FormatterRunOutcome {\n\tstatus: PolicyRunStatus;\n\tmutations: FormatterMutationReport;\n\tevidence: FormatterEvidence;\n\trefusalReason?: string;\n}\n\nexport interface FormatterRunOptions {\n\tworkspaceRoot: string;\n\tsignal?: AbortSignal;\n\tartifactStore?: WorkspaceArtifactStore;\n\t/** PS.1. A blocked formatter is refused before the snapshot and before the spawn. */\n\tauthorize?: AuthorizeConfiguredCommand;\n}\n\ninterface ScopeSnapshot {\n\t/** Workspace-relative path -> content fingerprint. */\n\thashes: Map<string, string>;\n\ttruncated: boolean;\n}\n\nfunction matchesAny(relPath: string, patterns: string[]): boolean {\n\treturn patterns.some((pattern) => minimatch(relPath, pattern, { dot: true }));\n}\n\nfunction fingerprint(buffer: Buffer): string {\n\treturn createHash(\"sha256\").update(buffer).digest(\"hex\");\n}\n\nfunction hashFile(absolute: string, size: number): string {\n\tif (size <= MAX_HASH_BYTES_PER_FILE) return fingerprint(readFileSync(absolute));\n\t// Oversized file: fingerprint the prefix and let the size field the\n\t// snapshot caller already compared catch whole-file rewrites.\n\treturn fingerprint(readFileSync(absolute).subarray(0, LARGE_FILE_PREFIX_BYTES));\n}\n\nfunction snapshotScope(root: string, priority: string[], background: string[]): ScopeSnapshot {\n\tconst hashes = new Map<string, string>();\n\tlet truncated = false;\n\tlet budgetFiles = MAX_SNAPSHOT_FILES;\n\tlet budgetBytes = MAX_SNAPSHOT_BYTES;\n\n\tconst walk = (dir: string): void => {\n\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\ttruncated = true;\n\t\t\treturn;\n\t\t}\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn; // unreadable directory: skip, never throw\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\t\ttruncated = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tif (!SKIPPED_DIRECTORIES.has(entry.name)) walk(join(dir, entry.name));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isFile() && !entry.isSymbolicLink()) continue;\n\t\t\tconst absolute = join(dir, entry.name);\n\t\t\tconst rel = relative(root, absolute).split(sep).join(\"/\");\n\t\t\tif (!matchesAny(rel, priority) && !matchesAny(rel, background)) continue;\n\t\t\tlet size: number;\n\t\t\ttry {\n\t\t\t\tsize = statSync(absolute).size;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgetFiles -= 1;\n\t\t\tif (size > budgetBytes) truncated = true;\n\t\t\ttry {\n\t\t\t\tbudgetBytes -= Math.min(size, MAX_HASH_BYTES_PER_FILE);\n\t\t\t\thashes.set(rel, hashFile(absolute, size));\n\t\t\t} catch {\n\t\t\t\t// presence without a fingerprint would lie; mark it honestly\n\t\t\t\thashes.set(rel, `unreadable:${size}`);\n\t\t\t}\n\t\t}\n\t};\n\n\twalk(resolve(root));\n\treturn { hashes, truncated };\n}\n\nfunction insideWorkspace(root: string, absolute: string): boolean {\n\t// The root itself may sit behind a symlink (macOS /tmp -> /private/tmp):\n\t// compare the changed path's realpath against the root's realpath, or\n\t// every workspace under a linked directory reads as an escape.\n\tlet realRoot = root;\n\ttry {\n\t\trealRoot = realpathSync(root);\n\t} catch {\n\t\t// root vanished between snapshots: compare as-given\n\t}\n\tconst rel = relative(realRoot, absolute);\n\treturn rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n}\n\nfunction diffScope(before: ScopeSnapshot, after: ScopeSnapshot): string[] {\n\tconst changed: string[] = [];\n\tfor (const [relPath, beforeHash] of before.hashes) {\n\t\tif (after.hashes.get(relPath) !== beforeHash) changed.push(relPath);\n\t}\n\tfor (const relPath of after.hashes.keys()) {\n\t\tif (!before.hashes.has(relPath)) changed.push(relPath);\n\t}\n\treturn changed.sort();\n}\n\n/**\n * Patterns from declaredPaths that remain inside pathScope. A pattern is\n * kept when it equals a scope pattern or a scope pattern is a directory\n * prefix of it; minimatch cannot express containment between globs, so\n * prefix containment is the conservative reading.\n */\nfunction intersectPatterns(declared: string[], scope: string[]): string[] {\n\treturn declared.filter((pattern) =>\n\t\tscope.some(\n\t\t\t(scopePattern) =>\n\t\t\t\tpattern === scopePattern ||\n\t\t\t\tpattern.startsWith(scopePattern.endsWith(\"/\") ? scopePattern : `${scopePattern}/`),\n\t\t),\n\t);\n}\n\nfunction emptyMutations(): FormatterMutationReport {\n\treturn { changedPaths: [], undeclaredPaths: [], escapedPaths: [], unchanged: true, truncatedSnapshot: false };\n}\n\n/** A formatter that never ran. Nothing was spawned, so the mutation report is empty by construction. */\nfunction refused(policy: FormatterPolicy, workspaceRoot: string, reason: string): FormatterRunOutcome {\n\treturn {\n\t\tstatus: \"refused\",\n\t\tmutations: emptyMutations(),\n\t\tevidence: {\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: workspaceRoot,\n\t\t\tstatus: \"refused\",\n\t\t\tdurationMs: 0,\n\t\t\ttruncated: false,\n\t\t},\n\t\trefusalReason: reason,\n\t};\n}\n\n/**\n * Copy the workspace into a private stage directory the formatter runs in.\n * Regular files only: a symlink is not reproduced, so a formatter cannot reach\n * a link's target through the stage, and the live link is never followed on\n * promotion either.\n */\nfunction materializeStage(root: string): { stageRoot: string; truncated: boolean } {\n\tconst stageRoot = mkdtempSync(join(tmpdir(), \"apex-formatter-stage-\"));\n\tlet truncated = false;\n\tlet budgetFiles = MAX_SNAPSHOT_FILES;\n\tlet budgetBytes = MAX_SNAPSHOT_BYTES;\n\n\tconst walk = (dir: string): void => {\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\t\ttruncated = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst absolute = join(dir, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tif (SKIPPED_DIRECTORIES.has(entry.name)) continue;\n\t\t\t\tmkdirSync(join(stageRoot, relative(root, absolute)), { recursive: true });\n\t\t\t\twalk(absolute);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isFile()) continue;\n\t\t\tlet size: number;\n\t\t\ttry {\n\t\t\t\tsize = statSync(absolute).size;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgetFiles -= 1;\n\t\t\tbudgetBytes -= size;\n\t\t\ttry {\n\t\t\t\tcopyFileSync(absolute, join(stageRoot, relative(root, absolute)));\n\t\t\t} catch {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t}\n\t};\n\n\twalk(root);\n\treturn { stageRoot, truncated };\n}\n\ninterface PromotionResult {\n\tpromoted: string[];\n\t/** Declared changes refused because the live file changed during the run. */\n\tconflicted: string[];\n}\n\n/**\n * Write the declared stage changes back into the live workspace through the\n * canonical no-follow write path (CA.2), so a symlink planted at the live\n * target is never followed. A file whose live bytes no longer match the\n * pre-run fingerprint belongs to a concurrent editor and is refused.\n */\nfunction promoteDeclaredChanges(\n\tworkspaceRoot: string,\n\tstageRoot: string,\n\tdeclaredChanges: string[],\n\tpreRun: Map<string, string>,\n): PromotionResult {\n\tconst promoted: string[] = [];\n\tconst conflicted: string[] = [];\n\tfor (const relPath of declaredChanges) {\n\t\tconst livePath = join(workspaceRoot, ...relPath.split(\"/\"));\n\t\tconst stagePath = join(stageRoot, ...relPath.split(\"/\"));\n\t\tlet liveHash: string | undefined;\n\t\ttry {\n\t\t\tliveHash = hashFile(livePath, statSync(livePath).size);\n\t\t} catch {\n\t\t\tliveHash = undefined;\n\t\t}\n\t\tif (liveHash !== preRun.get(relPath)) {\n\t\t\tconflicted.push(relPath);\n\t\t\tcontinue;\n\t\t}\n\t\tlet content: Buffer;\n\t\ttry {\n\t\t\tcontent = readFileSync(stagePath);\n\t\t} catch {\n\t\t\tconflicted.push(relPath);\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tmkdirSync(dirname(livePath), { recursive: true });\n\t\t\tconst prepared = preparePathOperation(livePath, workspaceRoot);\n\t\t\t// A symlink at the live target canonicalizes to whatever it points at.\n\t\t\t// Promotion writes only inside the workspace, so a target that resolves\n\t\t\t// out is refused rather than followed.\n\t\t\tif (!insideWorkspace(workspaceRoot, prepared.path.value)) {\n\t\t\t\tconflicted.push(relPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twritePreparedPath(prepared, content);\n\t\t\tpromoted.push(relPath);\n\t\t} catch {\n\t\t\tconflicted.push(relPath);\n\t\t}\n\t}\n\treturn { promoted, conflicted };\n}\n\nexport async function runFormatterCommand(\n\tpolicy: FormatterPolicy,\n\toptions: FormatterRunOptions,\n): Promise<FormatterRunOutcome> {\n\tconst workspaceRoot = resolve(options.workspaceRoot);\n\n\t// The loader rejects traversal and absolute scope patterns; this is the\n\t// defensive second gate, because the worst outcome here is running a\n\t// mutator whose declared scope points outside the workspace.\n\tconst scopePatterns = [...policy.declaredPaths, ...(policy.pathScope ?? [])];\n\tconst offending = scopePatterns.find((pattern) => pattern.includes(\"..\") || isAbsolute(pattern));\n\tif (offending !== undefined) {\n\t\treturn refused(\n\t\t\tpolicy,\n\t\t\tworkspaceRoot,\n\t\t\t`declared scope ${JSON.stringify(offending)} must stay inside the workspace`,\n\t\t);\n\t}\n\n\tconst tracked =\n\t\tpolicy.pathScope === undefined ? policy.declaredPaths : intersectPatterns(policy.declaredPaths, policy.pathScope);\n\n\tif (options.authorize !== undefined) {\n\t\tconst decision = await options.authorize({\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: workspaceRoot,\n\t\t\twriteScope: tracked,\n\t\t\tcapabilities: new Set([\"exec\", \"fs.write\"]),\n\t\t\tpermission: policy.permission,\n\t\t});\n\t\tif (decision.block) return refused(policy, workspaceRoot, decision.reason ?? \"not permitted\");\n\t}\n\n\t// Background coverage is everything: an unexpected mutation lives\n\t// outside every declared pattern, so only a whole-workspace view can\n\t// report it. Caps bound the walk; truncatedSnapshot carries the doubt.\n\tconst preRun = snapshotScope(workspaceRoot, tracked, [\"**\"]);\n\tconst stage = materializeStage(workspaceRoot);\n\tconst before = snapshotScope(stage.stageRoot, tracked, [\"**\"]);\n\tconst run = await runPolicyCommand(policy, {\n\t\tworkspaceRoot: stage.stageRoot,\n\t\tsignal: options.signal,\n\t\tartifactStore: options.artifactStore,\n\t});\n\tconst after = snapshotScope(stage.stageRoot, tracked, [\"**\"]);\n\n\tconst changedPaths = diffScope(before, after);\n\tconst undeclaredPaths: string[] = [];\n\tconst escapedPaths: string[] = [];\n\tfor (const relPath of changedPaths) {\n\t\tif (!matchesAny(relPath, tracked)) undeclaredPaths.push(relPath);\n\t\tconst absolute = join(stage.stageRoot, ...relPath.split(\"/\"));\n\t\ttry {\n\t\t\tconst real = realpathSync(absolute);\n\t\t\tif (!insideWorkspace(stage.stageRoot, real)) escapedPaths.push(relPath);\n\t\t} catch {\n\t\t\t// deleted or unresolvable between snapshots: no live escape to flag\n\t\t}\n\t}\n\n\tconst declaredChanges = changedPaths.filter(\n\t\t(relPath) => matchesAny(relPath, tracked) && !escapedPaths.includes(relPath),\n\t);\n\tconst promotion = promoteDeclaredChanges(workspaceRoot, stage.stageRoot, declaredChanges, preRun.hashes);\n\trmSync(stage.stageRoot, { recursive: true, force: true });\n\n\tconst outOfScope = undeclaredPaths.length > 0 || escapedPaths.length > 0;\n\tconst status: PolicyRunStatus =\n\t\trun.status !== \"passed\"\n\t\t\t? run.status\n\t\t\t: outOfScope\n\t\t\t\t? \"scope-violated\"\n\t\t\t\t: promotion.conflicted.length > 0\n\t\t\t\t\t? \"failed\"\n\t\t\t\t\t: \"passed\";\n\n\treturn {\n\t\tstatus,\n\t\tmutations: {\n\t\t\tchangedPaths,\n\t\t\tundeclaredPaths,\n\t\t\tescapedPaths,\n\t\t\tunchanged: changedPaths.length === 0,\n\t\t\ttruncatedSnapshot: preRun.truncated || before.truncated || after.truncated || stage.truncated,\n\t\t},\n\t\tevidence: {\n\t\t\tpolicyId: run.policyId,\n\t\t\texecutable: run.executable,\n\t\t\targv: run.argv,\n\t\t\tcwd: run.cwd,\n\t\t\tstatus,\n\t\t\tdurationMs: run.durationMs,\n\t\t\texitCode: run.exitCode,\n\t\t\tsignal: run.signal,\n\t\t\ttruncated: run.truncated,\n\t\t\tartifact: run.artifact,\n\t\t},\n\t\trefusalReason: run.refusalReason,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VF.5 plus PS.2: the formatter lifecycle. A formatter runs against an
|
|
3
|
+
* isolated copy of the workspace, never the workspace itself, and only the
|
|
4
|
+
* changes matching its declared paths are promoted back. An undeclared write
|
|
5
|
+
* is left behind in the discarded stage directory, so the live workspace never
|
|
6
|
+
* holds bytes the policy did not declare.
|
|
7
|
+
*
|
|
8
|
+
* This replaces post-hoc reporting. The earlier lifecycle ran the formatter in
|
|
9
|
+
* the live workspace and listed undeclared writes afterwards, which is a
|
|
10
|
+
* report, not a boundary: the stray bytes were already on disk. `status` is
|
|
11
|
+
* now `scope-violated` whenever a write fell outside the declared set, and
|
|
12
|
+
* that value can never read as `passed`.
|
|
13
|
+
*
|
|
14
|
+
* What this does NOT confine, deliberately and recorded rather than implied:
|
|
15
|
+
* a formatter writing to an absolute path outside the workspace, or reaching
|
|
16
|
+
* the network. Those are the OS sandbox's job. Copy plus restricted promotion
|
|
17
|
+
* confines workspace mutation, nothing wider.
|
|
18
|
+
*
|
|
19
|
+
* Promotion refuses any file whose live bytes changed during the run, because
|
|
20
|
+
* overwriting there would destroy a concurrent edit by the user. Such a run
|
|
21
|
+
* fails rather than reverting the user's work.
|
|
22
|
+
*
|
|
23
|
+
* The snapshot is a bounded walk (harness directories skipped, per-file and
|
|
24
|
+
* total byte caps). `truncatedSnapshot` is set when coverage was cut short, so
|
|
25
|
+
* an "unchanged" verdict can be read with the right suspicion.
|
|
26
|
+
*/
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, } from "node:fs";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
30
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
31
|
+
import { minimatch } from "minimatch";
|
|
32
|
+
import { runPolicyCommand } from "./policy-executor.js";
|
|
33
|
+
import { preparePathOperation, writePreparedPath } from "./tools/path-utils.js";
|
|
34
|
+
/** Per-file hash cap: larger files compare by size plus prefix hash. */
|
|
35
|
+
const MAX_HASH_BYTES_PER_FILE = 5 * 1024 * 1024;
|
|
36
|
+
/** Prefix length hashed when a file exceeds the per-file cap. */
|
|
37
|
+
const LARGE_FILE_PREFIX_BYTES = 64 * 1024;
|
|
38
|
+
/** File-count cap for the before/after walk. */
|
|
39
|
+
const MAX_SNAPSHOT_FILES = 2_000;
|
|
40
|
+
/** Total bytes hashed across one snapshot. */
|
|
41
|
+
const MAX_SNAPSHOT_BYTES = 32 * 1024 * 1024;
|
|
42
|
+
/** Directory names never part of a formatter's honest workspace view. */
|
|
43
|
+
const SKIPPED_DIRECTORIES = new Set([".git", "node_modules", ".apex-code", "sessions"]);
|
|
44
|
+
function matchesAny(relPath, patterns) {
|
|
45
|
+
return patterns.some((pattern) => minimatch(relPath, pattern, { dot: true }));
|
|
46
|
+
}
|
|
47
|
+
function fingerprint(buffer) {
|
|
48
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
function hashFile(absolute, size) {
|
|
51
|
+
if (size <= MAX_HASH_BYTES_PER_FILE)
|
|
52
|
+
return fingerprint(readFileSync(absolute));
|
|
53
|
+
// Oversized file: fingerprint the prefix and let the size field the
|
|
54
|
+
// snapshot caller already compared catch whole-file rewrites.
|
|
55
|
+
return fingerprint(readFileSync(absolute).subarray(0, LARGE_FILE_PREFIX_BYTES));
|
|
56
|
+
}
|
|
57
|
+
function snapshotScope(root, priority, background) {
|
|
58
|
+
const hashes = new Map();
|
|
59
|
+
let truncated = false;
|
|
60
|
+
let budgetFiles = MAX_SNAPSHOT_FILES;
|
|
61
|
+
let budgetBytes = MAX_SNAPSHOT_BYTES;
|
|
62
|
+
const walk = (dir) => {
|
|
63
|
+
if (budgetFiles <= 0 || budgetBytes <= 0) {
|
|
64
|
+
truncated = true;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return; // unreadable directory: skip, never throw
|
|
73
|
+
}
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (budgetFiles <= 0 || budgetBytes <= 0) {
|
|
76
|
+
truncated = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name))
|
|
81
|
+
walk(join(dir, entry.name));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
85
|
+
continue;
|
|
86
|
+
const absolute = join(dir, entry.name);
|
|
87
|
+
const rel = relative(root, absolute).split(sep).join("/");
|
|
88
|
+
if (!matchesAny(rel, priority) && !matchesAny(rel, background))
|
|
89
|
+
continue;
|
|
90
|
+
let size;
|
|
91
|
+
try {
|
|
92
|
+
size = statSync(absolute).size;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
budgetFiles -= 1;
|
|
98
|
+
if (size > budgetBytes)
|
|
99
|
+
truncated = true;
|
|
100
|
+
try {
|
|
101
|
+
budgetBytes -= Math.min(size, MAX_HASH_BYTES_PER_FILE);
|
|
102
|
+
hashes.set(rel, hashFile(absolute, size));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// presence without a fingerprint would lie; mark it honestly
|
|
106
|
+
hashes.set(rel, `unreadable:${size}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
walk(resolve(root));
|
|
111
|
+
return { hashes, truncated };
|
|
112
|
+
}
|
|
113
|
+
function insideWorkspace(root, absolute) {
|
|
114
|
+
// The root itself may sit behind a symlink (macOS /tmp -> /private/tmp):
|
|
115
|
+
// compare the changed path's realpath against the root's realpath, or
|
|
116
|
+
// every workspace under a linked directory reads as an escape.
|
|
117
|
+
let realRoot = root;
|
|
118
|
+
try {
|
|
119
|
+
realRoot = realpathSync(root);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// root vanished between snapshots: compare as-given
|
|
123
|
+
}
|
|
124
|
+
const rel = relative(realRoot, absolute);
|
|
125
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
126
|
+
}
|
|
127
|
+
function diffScope(before, after) {
|
|
128
|
+
const changed = [];
|
|
129
|
+
for (const [relPath, beforeHash] of before.hashes) {
|
|
130
|
+
if (after.hashes.get(relPath) !== beforeHash)
|
|
131
|
+
changed.push(relPath);
|
|
132
|
+
}
|
|
133
|
+
for (const relPath of after.hashes.keys()) {
|
|
134
|
+
if (!before.hashes.has(relPath))
|
|
135
|
+
changed.push(relPath);
|
|
136
|
+
}
|
|
137
|
+
return changed.sort();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Patterns from declaredPaths that remain inside pathScope. A pattern is
|
|
141
|
+
* kept when it equals a scope pattern or a scope pattern is a directory
|
|
142
|
+
* prefix of it; minimatch cannot express containment between globs, so
|
|
143
|
+
* prefix containment is the conservative reading.
|
|
144
|
+
*/
|
|
145
|
+
function intersectPatterns(declared, scope) {
|
|
146
|
+
return declared.filter((pattern) => scope.some((scopePattern) => pattern === scopePattern ||
|
|
147
|
+
pattern.startsWith(scopePattern.endsWith("/") ? scopePattern : `${scopePattern}/`)));
|
|
148
|
+
}
|
|
149
|
+
function emptyMutations() {
|
|
150
|
+
return { changedPaths: [], undeclaredPaths: [], escapedPaths: [], unchanged: true, truncatedSnapshot: false };
|
|
151
|
+
}
|
|
152
|
+
/** A formatter that never ran. Nothing was spawned, so the mutation report is empty by construction. */
|
|
153
|
+
function refused(policy, workspaceRoot, reason) {
|
|
154
|
+
return {
|
|
155
|
+
status: "refused",
|
|
156
|
+
mutations: emptyMutations(),
|
|
157
|
+
evidence: {
|
|
158
|
+
policyId: policy.id,
|
|
159
|
+
executable: policy.executable,
|
|
160
|
+
argv: policy.argv,
|
|
161
|
+
cwd: workspaceRoot,
|
|
162
|
+
status: "refused",
|
|
163
|
+
durationMs: 0,
|
|
164
|
+
truncated: false,
|
|
165
|
+
},
|
|
166
|
+
refusalReason: reason,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Copy the workspace into a private stage directory the formatter runs in.
|
|
171
|
+
* Regular files only: a symlink is not reproduced, so a formatter cannot reach
|
|
172
|
+
* a link's target through the stage, and the live link is never followed on
|
|
173
|
+
* promotion either.
|
|
174
|
+
*/
|
|
175
|
+
function materializeStage(root) {
|
|
176
|
+
const stageRoot = mkdtempSync(join(tmpdir(), "apex-formatter-stage-"));
|
|
177
|
+
let truncated = false;
|
|
178
|
+
let budgetFiles = MAX_SNAPSHOT_FILES;
|
|
179
|
+
let budgetBytes = MAX_SNAPSHOT_BYTES;
|
|
180
|
+
const walk = (dir) => {
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const entry of entries) {
|
|
189
|
+
if (budgetFiles <= 0 || budgetBytes <= 0) {
|
|
190
|
+
truncated = true;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const absolute = join(dir, entry.name);
|
|
194
|
+
if (entry.isDirectory()) {
|
|
195
|
+
if (SKIPPED_DIRECTORIES.has(entry.name))
|
|
196
|
+
continue;
|
|
197
|
+
mkdirSync(join(stageRoot, relative(root, absolute)), { recursive: true });
|
|
198
|
+
walk(absolute);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (!entry.isFile())
|
|
202
|
+
continue;
|
|
203
|
+
let size;
|
|
204
|
+
try {
|
|
205
|
+
size = statSync(absolute).size;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
budgetFiles -= 1;
|
|
211
|
+
budgetBytes -= size;
|
|
212
|
+
try {
|
|
213
|
+
copyFileSync(absolute, join(stageRoot, relative(root, absolute)));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
truncated = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
walk(root);
|
|
221
|
+
return { stageRoot, truncated };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Write the declared stage changes back into the live workspace through the
|
|
225
|
+
* canonical no-follow write path (CA.2), so a symlink planted at the live
|
|
226
|
+
* target is never followed. A file whose live bytes no longer match the
|
|
227
|
+
* pre-run fingerprint belongs to a concurrent editor and is refused.
|
|
228
|
+
*/
|
|
229
|
+
function promoteDeclaredChanges(workspaceRoot, stageRoot, declaredChanges, preRun) {
|
|
230
|
+
const promoted = [];
|
|
231
|
+
const conflicted = [];
|
|
232
|
+
for (const relPath of declaredChanges) {
|
|
233
|
+
const livePath = join(workspaceRoot, ...relPath.split("/"));
|
|
234
|
+
const stagePath = join(stageRoot, ...relPath.split("/"));
|
|
235
|
+
let liveHash;
|
|
236
|
+
try {
|
|
237
|
+
liveHash = hashFile(livePath, statSync(livePath).size);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
liveHash = undefined;
|
|
241
|
+
}
|
|
242
|
+
if (liveHash !== preRun.get(relPath)) {
|
|
243
|
+
conflicted.push(relPath);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
let content;
|
|
247
|
+
try {
|
|
248
|
+
content = readFileSync(stagePath);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
conflicted.push(relPath);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
mkdirSync(dirname(livePath), { recursive: true });
|
|
256
|
+
const prepared = preparePathOperation(livePath, workspaceRoot);
|
|
257
|
+
// A symlink at the live target canonicalizes to whatever it points at.
|
|
258
|
+
// Promotion writes only inside the workspace, so a target that resolves
|
|
259
|
+
// out is refused rather than followed.
|
|
260
|
+
if (!insideWorkspace(workspaceRoot, prepared.path.value)) {
|
|
261
|
+
conflicted.push(relPath);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
writePreparedPath(prepared, content);
|
|
265
|
+
promoted.push(relPath);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
conflicted.push(relPath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { promoted, conflicted };
|
|
272
|
+
}
|
|
273
|
+
export async function runFormatterCommand(policy, options) {
|
|
274
|
+
const workspaceRoot = resolve(options.workspaceRoot);
|
|
275
|
+
// The loader rejects traversal and absolute scope patterns; this is the
|
|
276
|
+
// defensive second gate, because the worst outcome here is running a
|
|
277
|
+
// mutator whose declared scope points outside the workspace.
|
|
278
|
+
const scopePatterns = [...policy.declaredPaths, ...(policy.pathScope ?? [])];
|
|
279
|
+
const offending = scopePatterns.find((pattern) => pattern.includes("..") || isAbsolute(pattern));
|
|
280
|
+
if (offending !== undefined) {
|
|
281
|
+
return refused(policy, workspaceRoot, `declared scope ${JSON.stringify(offending)} must stay inside the workspace`);
|
|
282
|
+
}
|
|
283
|
+
const tracked = policy.pathScope === undefined ? policy.declaredPaths : intersectPatterns(policy.declaredPaths, policy.pathScope);
|
|
284
|
+
if (options.authorize !== undefined) {
|
|
285
|
+
const decision = await options.authorize({
|
|
286
|
+
policyId: policy.id,
|
|
287
|
+
executable: policy.executable,
|
|
288
|
+
argv: policy.argv,
|
|
289
|
+
cwd: workspaceRoot,
|
|
290
|
+
writeScope: tracked,
|
|
291
|
+
capabilities: new Set(["exec", "fs.write"]),
|
|
292
|
+
permission: policy.permission,
|
|
293
|
+
});
|
|
294
|
+
if (decision.block)
|
|
295
|
+
return refused(policy, workspaceRoot, decision.reason ?? "not permitted");
|
|
296
|
+
}
|
|
297
|
+
// Background coverage is everything: an unexpected mutation lives
|
|
298
|
+
// outside every declared pattern, so only a whole-workspace view can
|
|
299
|
+
// report it. Caps bound the walk; truncatedSnapshot carries the doubt.
|
|
300
|
+
const preRun = snapshotScope(workspaceRoot, tracked, ["**"]);
|
|
301
|
+
const stage = materializeStage(workspaceRoot);
|
|
302
|
+
const before = snapshotScope(stage.stageRoot, tracked, ["**"]);
|
|
303
|
+
const run = await runPolicyCommand(policy, {
|
|
304
|
+
workspaceRoot: stage.stageRoot,
|
|
305
|
+
signal: options.signal,
|
|
306
|
+
artifactStore: options.artifactStore,
|
|
307
|
+
});
|
|
308
|
+
const after = snapshotScope(stage.stageRoot, tracked, ["**"]);
|
|
309
|
+
const changedPaths = diffScope(before, after);
|
|
310
|
+
const undeclaredPaths = [];
|
|
311
|
+
const escapedPaths = [];
|
|
312
|
+
for (const relPath of changedPaths) {
|
|
313
|
+
if (!matchesAny(relPath, tracked))
|
|
314
|
+
undeclaredPaths.push(relPath);
|
|
315
|
+
const absolute = join(stage.stageRoot, ...relPath.split("/"));
|
|
316
|
+
try {
|
|
317
|
+
const real = realpathSync(absolute);
|
|
318
|
+
if (!insideWorkspace(stage.stageRoot, real))
|
|
319
|
+
escapedPaths.push(relPath);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// deleted or unresolvable between snapshots: no live escape to flag
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const declaredChanges = changedPaths.filter((relPath) => matchesAny(relPath, tracked) && !escapedPaths.includes(relPath));
|
|
326
|
+
const promotion = promoteDeclaredChanges(workspaceRoot, stage.stageRoot, declaredChanges, preRun.hashes);
|
|
327
|
+
rmSync(stage.stageRoot, { recursive: true, force: true });
|
|
328
|
+
const outOfScope = undeclaredPaths.length > 0 || escapedPaths.length > 0;
|
|
329
|
+
const status = run.status !== "passed"
|
|
330
|
+
? run.status
|
|
331
|
+
: outOfScope
|
|
332
|
+
? "scope-violated"
|
|
333
|
+
: promotion.conflicted.length > 0
|
|
334
|
+
? "failed"
|
|
335
|
+
: "passed";
|
|
336
|
+
return {
|
|
337
|
+
status,
|
|
338
|
+
mutations: {
|
|
339
|
+
changedPaths,
|
|
340
|
+
undeclaredPaths,
|
|
341
|
+
escapedPaths,
|
|
342
|
+
unchanged: changedPaths.length === 0,
|
|
343
|
+
truncatedSnapshot: preRun.truncated || before.truncated || after.truncated || stage.truncated,
|
|
344
|
+
},
|
|
345
|
+
evidence: {
|
|
346
|
+
policyId: run.policyId,
|
|
347
|
+
executable: run.executable,
|
|
348
|
+
argv: run.argv,
|
|
349
|
+
cwd: run.cwd,
|
|
350
|
+
status,
|
|
351
|
+
durationMs: run.durationMs,
|
|
352
|
+
exitCode: run.exitCode,
|
|
353
|
+
signal: run.signal,
|
|
354
|
+
truncated: run.truncated,
|
|
355
|
+
artifact: run.artifact,
|
|
356
|
+
},
|
|
357
|
+
refusalReason: run.refusalReason,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=formatter-lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter-lifecycle.js","sourceRoot":"","sources":["../../src/core/formatter-lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * VF.5 plus PS.2: the formatter lifecycle. A formatter runs against an\n * isolated copy of the workspace, never the workspace itself, and only the\n * changes matching its declared paths are promoted back. An undeclared write\n * is left behind in the discarded stage directory, so the live workspace never\n * holds bytes the policy did not declare.\n *\n * This replaces post-hoc reporting. The earlier lifecycle ran the formatter in\n * the live workspace and listed undeclared writes afterwards, which is a\n * report, not a boundary: the stray bytes were already on disk. `status` is\n * now `scope-violated` whenever a write fell outside the declared set, and\n * that value can never read as `passed`.\n *\n * What this does NOT confine, deliberately and recorded rather than implied:\n * a formatter writing to an absolute path outside the workspace, or reaching\n * the network. Those are the OS sandbox's job. Copy plus restricted promotion\n * confines workspace mutation, nothing wider.\n *\n * Promotion refuses any file whose live bytes changed during the run, because\n * overwriting there would destroy a concurrent edit by the user. Such a run\n * fails rather than reverting the user's work.\n *\n * The snapshot is a bounded walk (harness directories skipped, per-file and\n * total byte caps). `truncatedSnapshot` is set when coverage was cut short, so\n * an \"unchanged\" verdict can be read with the right suspicion.\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n\tcopyFileSync,\n\ttype Dirent,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\trealpathSync,\n\trmSync,\n\tstatSync,\n} from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { minimatch } from \"minimatch\";\nimport type { AuthorizeConfiguredCommand } from \"./permissions/policy-command.ts\";\nimport { type PolicyRunStatus, runPolicyCommand } from \"./policy-executor.ts\";\nimport type { FormatterPolicy } from \"./policy-loader.ts\";\nimport { preparePathOperation, writePreparedPath } from \"./tools/path-utils.ts\";\nimport type { WorkspaceArtifactRef, WorkspaceArtifactStore } from \"./workspace/artifacts.ts\";\n\n/** Per-file hash cap: larger files compare by size plus prefix hash. */\nconst MAX_HASH_BYTES_PER_FILE = 5 * 1024 * 1024;\n/** Prefix length hashed when a file exceeds the per-file cap. */\nconst LARGE_FILE_PREFIX_BYTES = 64 * 1024;\n/** File-count cap for the before/after walk. */\nconst MAX_SNAPSHOT_FILES = 2_000;\n/** Total bytes hashed across one snapshot. */\nconst MAX_SNAPSHOT_BYTES = 32 * 1024 * 1024;\n/** Directory names never part of a formatter's honest workspace view. */\nconst SKIPPED_DIRECTORIES = new Set([\".git\", \"node_modules\", \".apex-code\", \"sessions\"]);\n\nexport interface FormatterMutationReport {\n\t/** Workspace-relative paths (forward slashes) that differ before vs after. */\n\tchangedPaths: string[];\n\t/** Changed paths the policy did not declare inside its pathScope. */\n\tundeclaredPaths: string[];\n\t/** Changed paths whose real location resolves outside the workspace. */\n\tescapedPaths: string[];\n\t/** True when nothing in the snapshot differs. */\n\tunchanged: boolean;\n\t/** True when the walk hit its caps before covering the whole workspace. */\n\ttruncatedSnapshot: boolean;\n}\n\nexport interface FormatterEvidence {\n\tpolicyId: string;\n\texecutable: string;\n\targv: string[];\n\tcwd: string;\n\tstatus: PolicyRunStatus;\n\tdurationMs: number;\n\texitCode?: number;\n\tsignal?: string;\n\ttruncated: boolean;\n\tartifact?: WorkspaceArtifactRef;\n}\n\nexport interface FormatterRunOutcome {\n\tstatus: PolicyRunStatus;\n\tmutations: FormatterMutationReport;\n\tevidence: FormatterEvidence;\n\trefusalReason?: string;\n}\n\nexport interface FormatterRunOptions {\n\tworkspaceRoot: string;\n\tsignal?: AbortSignal;\n\tartifactStore?: WorkspaceArtifactStore;\n\t/** PS.1. A blocked formatter is refused before the snapshot and before the spawn. */\n\tauthorize?: AuthorizeConfiguredCommand;\n}\n\ninterface ScopeSnapshot {\n\t/** Workspace-relative path -> content fingerprint. */\n\thashes: Map<string, string>;\n\ttruncated: boolean;\n}\n\nfunction matchesAny(relPath: string, patterns: string[]): boolean {\n\treturn patterns.some((pattern) => minimatch(relPath, pattern, { dot: true }));\n}\n\nfunction fingerprint(buffer: Buffer): string {\n\treturn createHash(\"sha256\").update(buffer).digest(\"hex\");\n}\n\nfunction hashFile(absolute: string, size: number): string {\n\tif (size <= MAX_HASH_BYTES_PER_FILE) return fingerprint(readFileSync(absolute));\n\t// Oversized file: fingerprint the prefix and let the size field the\n\t// snapshot caller already compared catch whole-file rewrites.\n\treturn fingerprint(readFileSync(absolute).subarray(0, LARGE_FILE_PREFIX_BYTES));\n}\n\nfunction snapshotScope(root: string, priority: string[], background: string[]): ScopeSnapshot {\n\tconst hashes = new Map<string, string>();\n\tlet truncated = false;\n\tlet budgetFiles = MAX_SNAPSHOT_FILES;\n\tlet budgetBytes = MAX_SNAPSHOT_BYTES;\n\n\tconst walk = (dir: string): void => {\n\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\ttruncated = true;\n\t\t\treturn;\n\t\t}\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn; // unreadable directory: skip, never throw\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\t\ttruncated = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tif (!SKIPPED_DIRECTORIES.has(entry.name)) walk(join(dir, entry.name));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isFile() && !entry.isSymbolicLink()) continue;\n\t\t\tconst absolute = join(dir, entry.name);\n\t\t\tconst rel = relative(root, absolute).split(sep).join(\"/\");\n\t\t\tif (!matchesAny(rel, priority) && !matchesAny(rel, background)) continue;\n\t\t\tlet size: number;\n\t\t\ttry {\n\t\t\t\tsize = statSync(absolute).size;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgetFiles -= 1;\n\t\t\tif (size > budgetBytes) truncated = true;\n\t\t\ttry {\n\t\t\t\tbudgetBytes -= Math.min(size, MAX_HASH_BYTES_PER_FILE);\n\t\t\t\thashes.set(rel, hashFile(absolute, size));\n\t\t\t} catch {\n\t\t\t\t// presence without a fingerprint would lie; mark it honestly\n\t\t\t\thashes.set(rel, `unreadable:${size}`);\n\t\t\t}\n\t\t}\n\t};\n\n\twalk(resolve(root));\n\treturn { hashes, truncated };\n}\n\nfunction insideWorkspace(root: string, absolute: string): boolean {\n\t// The root itself may sit behind a symlink (macOS /tmp -> /private/tmp):\n\t// compare the changed path's realpath against the root's realpath, or\n\t// every workspace under a linked directory reads as an escape.\n\tlet realRoot = root;\n\ttry {\n\t\trealRoot = realpathSync(root);\n\t} catch {\n\t\t// root vanished between snapshots: compare as-given\n\t}\n\tconst rel = relative(realRoot, absolute);\n\treturn rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n}\n\nfunction diffScope(before: ScopeSnapshot, after: ScopeSnapshot): string[] {\n\tconst changed: string[] = [];\n\tfor (const [relPath, beforeHash] of before.hashes) {\n\t\tif (after.hashes.get(relPath) !== beforeHash) changed.push(relPath);\n\t}\n\tfor (const relPath of after.hashes.keys()) {\n\t\tif (!before.hashes.has(relPath)) changed.push(relPath);\n\t}\n\treturn changed.sort();\n}\n\n/**\n * Patterns from declaredPaths that remain inside pathScope. A pattern is\n * kept when it equals a scope pattern or a scope pattern is a directory\n * prefix of it; minimatch cannot express containment between globs, so\n * prefix containment is the conservative reading.\n */\nfunction intersectPatterns(declared: string[], scope: string[]): string[] {\n\treturn declared.filter((pattern) =>\n\t\tscope.some(\n\t\t\t(scopePattern) =>\n\t\t\t\tpattern === scopePattern ||\n\t\t\t\tpattern.startsWith(scopePattern.endsWith(\"/\") ? scopePattern : `${scopePattern}/`),\n\t\t),\n\t);\n}\n\nfunction emptyMutations(): FormatterMutationReport {\n\treturn { changedPaths: [], undeclaredPaths: [], escapedPaths: [], unchanged: true, truncatedSnapshot: false };\n}\n\n/** A formatter that never ran. Nothing was spawned, so the mutation report is empty by construction. */\nfunction refused(policy: FormatterPolicy, workspaceRoot: string, reason: string): FormatterRunOutcome {\n\treturn {\n\t\tstatus: \"refused\",\n\t\tmutations: emptyMutations(),\n\t\tevidence: {\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: workspaceRoot,\n\t\t\tstatus: \"refused\",\n\t\t\tdurationMs: 0,\n\t\t\ttruncated: false,\n\t\t},\n\t\trefusalReason: reason,\n\t};\n}\n\n/**\n * Copy the workspace into a private stage directory the formatter runs in.\n * Regular files only: a symlink is not reproduced, so a formatter cannot reach\n * a link's target through the stage, and the live link is never followed on\n * promotion either.\n */\nfunction materializeStage(root: string): { stageRoot: string; truncated: boolean } {\n\tconst stageRoot = mkdtempSync(join(tmpdir(), \"apex-formatter-stage-\"));\n\tlet truncated = false;\n\tlet budgetFiles = MAX_SNAPSHOT_FILES;\n\tlet budgetBytes = MAX_SNAPSHOT_BYTES;\n\n\tconst walk = (dir: string): void => {\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (budgetFiles <= 0 || budgetBytes <= 0) {\n\t\t\t\ttruncated = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst absolute = join(dir, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tif (SKIPPED_DIRECTORIES.has(entry.name)) continue;\n\t\t\t\tmkdirSync(join(stageRoot, relative(root, absolute)), { recursive: true });\n\t\t\t\twalk(absolute);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isFile()) continue;\n\t\t\tlet size: number;\n\t\t\ttry {\n\t\t\t\tsize = statSync(absolute).size;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbudgetFiles -= 1;\n\t\t\tbudgetBytes -= size;\n\t\t\ttry {\n\t\t\t\tcopyFileSync(absolute, join(stageRoot, relative(root, absolute)));\n\t\t\t} catch {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t}\n\t};\n\n\twalk(root);\n\treturn { stageRoot, truncated };\n}\n\ninterface PromotionResult {\n\tpromoted: string[];\n\t/** Declared changes refused because the live file changed during the run. */\n\tconflicted: string[];\n}\n\n/**\n * Write the declared stage changes back into the live workspace through the\n * canonical no-follow write path (CA.2), so a symlink planted at the live\n * target is never followed. A file whose live bytes no longer match the\n * pre-run fingerprint belongs to a concurrent editor and is refused.\n */\nfunction promoteDeclaredChanges(\n\tworkspaceRoot: string,\n\tstageRoot: string,\n\tdeclaredChanges: string[],\n\tpreRun: Map<string, string>,\n): PromotionResult {\n\tconst promoted: string[] = [];\n\tconst conflicted: string[] = [];\n\tfor (const relPath of declaredChanges) {\n\t\tconst livePath = join(workspaceRoot, ...relPath.split(\"/\"));\n\t\tconst stagePath = join(stageRoot, ...relPath.split(\"/\"));\n\t\tlet liveHash: string | undefined;\n\t\ttry {\n\t\t\tliveHash = hashFile(livePath, statSync(livePath).size);\n\t\t} catch {\n\t\t\tliveHash = undefined;\n\t\t}\n\t\tif (liveHash !== preRun.get(relPath)) {\n\t\t\tconflicted.push(relPath);\n\t\t\tcontinue;\n\t\t}\n\t\tlet content: Buffer;\n\t\ttry {\n\t\t\tcontent = readFileSync(stagePath);\n\t\t} catch {\n\t\t\tconflicted.push(relPath);\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\tmkdirSync(dirname(livePath), { recursive: true });\n\t\t\tconst prepared = preparePathOperation(livePath, workspaceRoot);\n\t\t\t// A symlink at the live target canonicalizes to whatever it points at.\n\t\t\t// Promotion writes only inside the workspace, so a target that resolves\n\t\t\t// out is refused rather than followed.\n\t\t\tif (!insideWorkspace(workspaceRoot, prepared.path.value)) {\n\t\t\t\tconflicted.push(relPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twritePreparedPath(prepared, content);\n\t\t\tpromoted.push(relPath);\n\t\t} catch {\n\t\t\tconflicted.push(relPath);\n\t\t}\n\t}\n\treturn { promoted, conflicted };\n}\n\nexport async function runFormatterCommand(\n\tpolicy: FormatterPolicy,\n\toptions: FormatterRunOptions,\n): Promise<FormatterRunOutcome> {\n\tconst workspaceRoot = resolve(options.workspaceRoot);\n\n\t// The loader rejects traversal and absolute scope patterns; this is the\n\t// defensive second gate, because the worst outcome here is running a\n\t// mutator whose declared scope points outside the workspace.\n\tconst scopePatterns = [...policy.declaredPaths, ...(policy.pathScope ?? [])];\n\tconst offending = scopePatterns.find((pattern) => pattern.includes(\"..\") || isAbsolute(pattern));\n\tif (offending !== undefined) {\n\t\treturn refused(\n\t\t\tpolicy,\n\t\t\tworkspaceRoot,\n\t\t\t`declared scope ${JSON.stringify(offending)} must stay inside the workspace`,\n\t\t);\n\t}\n\n\tconst tracked =\n\t\tpolicy.pathScope === undefined ? policy.declaredPaths : intersectPatterns(policy.declaredPaths, policy.pathScope);\n\n\tif (options.authorize !== undefined) {\n\t\tconst decision = await options.authorize({\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: workspaceRoot,\n\t\t\twriteScope: tracked,\n\t\t\tcapabilities: new Set([\"exec\", \"fs.write\"]),\n\t\t\tpermission: policy.permission,\n\t\t});\n\t\tif (decision.block) return refused(policy, workspaceRoot, decision.reason ?? \"not permitted\");\n\t}\n\n\t// Background coverage is everything: an unexpected mutation lives\n\t// outside every declared pattern, so only a whole-workspace view can\n\t// report it. Caps bound the walk; truncatedSnapshot carries the doubt.\n\tconst preRun = snapshotScope(workspaceRoot, tracked, [\"**\"]);\n\tconst stage = materializeStage(workspaceRoot);\n\tconst before = snapshotScope(stage.stageRoot, tracked, [\"**\"]);\n\tconst run = await runPolicyCommand(policy, {\n\t\tworkspaceRoot: stage.stageRoot,\n\t\tsignal: options.signal,\n\t\tartifactStore: options.artifactStore,\n\t});\n\tconst after = snapshotScope(stage.stageRoot, tracked, [\"**\"]);\n\n\tconst changedPaths = diffScope(before, after);\n\tconst undeclaredPaths: string[] = [];\n\tconst escapedPaths: string[] = [];\n\tfor (const relPath of changedPaths) {\n\t\tif (!matchesAny(relPath, tracked)) undeclaredPaths.push(relPath);\n\t\tconst absolute = join(stage.stageRoot, ...relPath.split(\"/\"));\n\t\ttry {\n\t\t\tconst real = realpathSync(absolute);\n\t\t\tif (!insideWorkspace(stage.stageRoot, real)) escapedPaths.push(relPath);\n\t\t} catch {\n\t\t\t// deleted or unresolvable between snapshots: no live escape to flag\n\t\t}\n\t}\n\n\tconst declaredChanges = changedPaths.filter(\n\t\t(relPath) => matchesAny(relPath, tracked) && !escapedPaths.includes(relPath),\n\t);\n\tconst promotion = promoteDeclaredChanges(workspaceRoot, stage.stageRoot, declaredChanges, preRun.hashes);\n\trmSync(stage.stageRoot, { recursive: true, force: true });\n\n\tconst outOfScope = undeclaredPaths.length > 0 || escapedPaths.length > 0;\n\tconst status: PolicyRunStatus =\n\t\trun.status !== \"passed\"\n\t\t\t? run.status\n\t\t\t: outOfScope\n\t\t\t\t? \"scope-violated\"\n\t\t\t\t: promotion.conflicted.length > 0\n\t\t\t\t\t? \"failed\"\n\t\t\t\t\t: \"passed\";\n\n\treturn {\n\t\tstatus,\n\t\tmutations: {\n\t\t\tchangedPaths,\n\t\t\tundeclaredPaths,\n\t\t\tescapedPaths,\n\t\t\tunchanged: changedPaths.length === 0,\n\t\t\ttruncatedSnapshot: preRun.truncated || before.truncated || after.truncated || stage.truncated,\n\t\t},\n\t\tevidence: {\n\t\t\tpolicyId: run.policyId,\n\t\t\texecutable: run.executable,\n\t\t\targv: run.argv,\n\t\t\tcwd: run.cwd,\n\t\t\tstatus,\n\t\t\tdurationMs: run.durationMs,\n\t\t\texitCode: run.exitCode,\n\t\t\tsignal: run.signal,\n\t\t\ttruncated: run.truncated,\n\t\t\tartifact: run.artifact,\n\t\t},\n\t\trefusalReason: run.refusalReason,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type HookCommandHandlerConfig, type HookHandler, type HookOutcome } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Shared decision parsing for command stdout and HTTP response bodies.
|
|
4
|
+
* Non-JSON output, a non-object, or an unknown decision value is "no decision"
|
|
5
|
+
* plus a warning -- never an allow (spec, Risks).
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseHookDecisionOutput(text: string): HookOutcome;
|
|
8
|
+
/**
|
|
9
|
+
* A command handler runs through the platform shell (`sh -c`, PowerShell on
|
|
10
|
+
* Windows) with the event payload on stdin and its decision on stdout.
|
|
11
|
+
*
|
|
12
|
+
* Exit-code table (behavioral reference:
|
|
13
|
+
* docs/research/2026-08-31-harness-landscape.md § 2.1): exit 0 + JSON = that
|
|
14
|
+
* decision; exit 0 + no output = no decision; exit 2 = block with stderr as
|
|
15
|
+
* the reason; any other nonzero exit (and spawn failure, and timeout) is a
|
|
16
|
+
* failure outcome, which the runtime turns into a fail-closed block.
|
|
17
|
+
*/
|
|
18
|
+
export declare function commandHookHandler(config: HookCommandHandlerConfig): HookHandler;
|
|
19
|
+
//# sourceMappingURL=command-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-handler.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/command-handler.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,wBAAwB,EAE7B,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,MAAM,YAAY,CAAC;AAUpB;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAsBjE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,WAAW,CAwDhF","sourcesContent":["import { spawn } from \"node:child_process\";\nimport {\n\tDEFAULT_HOOK_TIMEOUT_MS,\n\ttype HookCommandHandlerConfig,\n\ttype HookEventPayload,\n\ttype HookHandler,\n\ttype HookOutcome,\n} from \"./types.ts\";\n\n// A runaway formatter can emit megabytes; the decision that matters is at the\n// start, so cap what we retain rather than what the child may write.\nconst MAX_HOOK_OUTPUT_CHARS = 1_000_000;\n\nfunction truncate(text: string): string {\n\treturn text.length > 200 ? `${text.slice(0, 200)}...` : text;\n}\n\n/**\n * Shared decision parsing for command stdout and HTTP response bodies.\n * Non-JSON output, a non-object, or an unknown decision value is \"no decision\"\n * plus a warning -- never an allow (spec, Risks).\n */\nexport function parseHookDecisionOutput(text: string): HookOutcome {\n\tconst trimmed = text.trim();\n\tif (trimmed.length === 0) return { ok: true };\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(trimmed);\n\t} catch {\n\t\treturn { ok: true, warning: `hook output was not valid JSON: ${truncate(trimmed)}` };\n\t}\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\treturn { ok: true, warning: `hook output was not a JSON object: ${truncate(trimmed)}` };\n\t}\n\tconst decision = (parsed as { decision?: unknown }).decision;\n\tif (decision === undefined) return { ok: true };\n\tif (decision !== \"allow\" && decision !== \"block\" && decision !== \"ask\") {\n\t\treturn { ok: true, warning: `hook output had an unknown decision: ${truncate(String(decision))}` };\n\t}\n\tconst reason = (parsed as { reason?: unknown }).reason;\n\treturn {\n\t\tok: true,\n\t\tdecision: { decision, ...(typeof reason === \"string\" && reason.length > 0 ? { reason } : {}) },\n\t};\n}\n\n/**\n * A command handler runs through the platform shell (`sh -c`, PowerShell on\n * Windows) with the event payload on stdin and its decision on stdout.\n *\n * Exit-code table (behavioral reference:\n * docs/research/2026-08-31-harness-landscape.md § 2.1): exit 0 + JSON = that\n * decision; exit 0 + no output = no decision; exit 2 = block with stderr as\n * the reason; any other nonzero exit (and spawn failure, and timeout) is a\n * failure outcome, which the runtime turns into a fail-closed block.\n */\nexport function commandHookHandler(config: HookCommandHandlerConfig): HookHandler {\n\tconst timeoutMs = config.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS;\n\tconst commandRef = truncate(config.command);\n\treturn {\n\t\tasync execute(payload: HookEventPayload): Promise<HookOutcome> {\n\t\t\tconst shell = process.platform === \"win32\" ? \"powershell.exe\" : \"sh\";\n\t\t\tconst args =\n\t\t\t\tprocess.platform === \"win32\" ? [\"-NoProfile\", \"-Command\", config.command] : [\"-c\", config.command];\n\t\t\treturn await new Promise<HookOutcome>((resolve) => {\n\t\t\t\tconst child = spawn(shell, args, { stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n\t\t\t\tlet stdout = \"\";\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tlet settled = false;\n\t\t\t\tconst finish = (outcome: HookOutcome) => {\n\t\t\t\t\tif (settled) return;\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve(outcome);\n\t\t\t\t};\n\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\tchild.kill();\n\t\t\t\t\tfinish({ ok: false, warning: `hook timed out after ${timeoutMs}ms: ${commandRef}` });\n\t\t\t\t}, timeoutMs);\n\t\t\t\tchild.stdout.on(\"data\", (chunk: Buffer) => {\n\t\t\t\t\tif (stdout.length < MAX_HOOK_OUTPUT_CHARS) stdout += chunk.toString(\"utf8\");\n\t\t\t\t});\n\t\t\t\tchild.stderr.on(\"data\", (chunk: Buffer) => {\n\t\t\t\t\tif (stderr.length < MAX_HOOK_OUTPUT_CHARS) stderr += chunk.toString(\"utf8\");\n\t\t\t\t});\n\t\t\t\t// A hook that closes stdin early must not crash the write; the exit code decides.\n\t\t\t\tchild.stdin.on(\"error\", () => {});\n\t\t\t\tchild.on(\"error\", (error) => finish({ ok: false, warning: `hook failed to spawn: ${error.message}` }));\n\t\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\t\tif (code === 0) {\n\t\t\t\t\t\tfinish(parseHookDecisionOutput(stdout));\n\t\t\t\t\t} else if (code === 2) {\n\t\t\t\t\t\tfinish({\n\t\t\t\t\t\t\tok: true,\n\t\t\t\t\t\t\tdecision: {\n\t\t\t\t\t\t\t\tdecision: \"block\",\n\t\t\t\t\t\t\t\treason: stderr.trim() || stdout.trim() || \"blocked by hook (exit 2)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (code === null) {\n\t\t\t\t\t\tfinish({ ok: false, warning: `hook terminated by a signal: ${commandRef}` });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfinish({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\twarning: `hook exited with exit code ${code}: ${stderr.trim() || stdout.trim() || commandRef}`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tchild.stdin.end(JSON.stringify(payload));\n\t\t\t});\n\t\t},\n\t};\n}\n"]}
|