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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VF.4: the verification lifecycle (spec
|
|
3
|
+
* 2026-09-01-configured-verification-and-formatting.md § 4). The tracker
|
|
4
|
+
* turns bounded executor outcomes into the five completion statuses the
|
|
5
|
+
* spec names — verified, failed, unavailable, interrupted,
|
|
6
|
+
* continued-unverified — and retires a result the moment the workspace it
|
|
7
|
+
* described changes. It never flips failed to passed, and a stale result
|
|
8
|
+
* never presents as current: the conservative rule is that ANY non-empty
|
|
9
|
+
* workspace change retires the record, because proving a change irrelevant
|
|
10
|
+
* is the executor's job, not the lifecycle's.
|
|
11
|
+
*
|
|
12
|
+
* Evidence is bounded by construction: the record carries policy identity,
|
|
13
|
+
* argv, status, duration, exit/signal/truncation flags, and the artifact
|
|
14
|
+
* reference — never raw output. Callers that want output read the artifact
|
|
15
|
+
* through the store's permission gate.
|
|
16
|
+
*/
|
|
17
|
+
import type { AuthorizeConfiguredCommand } from "./permissions/policy-command.ts";
|
|
18
|
+
import type { PolicyRunStatus } from "./policy-executor.ts";
|
|
19
|
+
import type { VerificationPolicy } from "./policy-loader.ts";
|
|
20
|
+
import type { WorkspaceArtifactRef } from "./workspace/artifacts.ts";
|
|
21
|
+
export type VerificationOutcome = "verified" | "failed" | "interrupted";
|
|
22
|
+
export type VerificationCompletionStatus = "verified" | "failed" | "unavailable" | "interrupted" | "continued-unverified";
|
|
23
|
+
export interface VerificationEvidence {
|
|
24
|
+
policyId: string;
|
|
25
|
+
executable: string;
|
|
26
|
+
argv: string[];
|
|
27
|
+
cwd: string;
|
|
28
|
+
status: PolicyRunStatus;
|
|
29
|
+
durationMs: number;
|
|
30
|
+
exitCode?: number;
|
|
31
|
+
signal?: string;
|
|
32
|
+
truncated: boolean;
|
|
33
|
+
artifact?: WorkspaceArtifactRef;
|
|
34
|
+
}
|
|
35
|
+
export interface VerificationRecord {
|
|
36
|
+
outcome: VerificationOutcome;
|
|
37
|
+
evidence: VerificationEvidence[];
|
|
38
|
+
observedAt: number;
|
|
39
|
+
}
|
|
40
|
+
export type VerificationBoundary = "explicit" | "post-turn";
|
|
41
|
+
export interface VerificationTrackerOptions {
|
|
42
|
+
workspaceRoot: string;
|
|
43
|
+
/**
|
|
44
|
+
* PS.1. Absent only in tests and embeddings with no permission authority at
|
|
45
|
+
* all; a session that has a gate always supplies one, and a blocked policy
|
|
46
|
+
* then spawns nothing.
|
|
47
|
+
*/
|
|
48
|
+
authorize?: AuthorizeConfiguredCommand;
|
|
49
|
+
}
|
|
50
|
+
export declare class VerificationTracker {
|
|
51
|
+
#private;
|
|
52
|
+
constructor(options: VerificationTrackerOptions);
|
|
53
|
+
configure(policies: VerificationPolicy[], boundary: VerificationBoundary): void;
|
|
54
|
+
get boundary(): VerificationBoundary;
|
|
55
|
+
policyIds(): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Run one configured policy by id (default: the first). Returns the new
|
|
58
|
+
* record, or undefined when the id is not configured — surfaced as
|
|
59
|
+
* "unavailable" by completionStatus(). A run retires any earlier record
|
|
60
|
+
* and any earlier continue-without-verification choice.
|
|
61
|
+
*/
|
|
62
|
+
runExplicit(policyId?: string, options?: {
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}): Promise<VerificationRecord | undefined>;
|
|
65
|
+
/** The user chose to finish without verification. Its own status, not a failure. */
|
|
66
|
+
recordContinuedWithoutVerification(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Report a workspace change observed after the latest record. Any
|
|
69
|
+
* non-empty change set retires the record: the result described a
|
|
70
|
+
* workspace that no longer exists.
|
|
71
|
+
*/
|
|
72
|
+
noteWorkspaceChange(changedPaths: ReadonlySet<string>): void;
|
|
73
|
+
completionStatus(): VerificationCompletionStatus;
|
|
74
|
+
/** The current record, if one is live. Stale results are not returned. */
|
|
75
|
+
latest(): VerificationRecord | undefined;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=verification-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verification-lifecycle.d.ts","sourceRoot":"","sources":["../../src/core/verification-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAClF,OAAO,KAAK,EAAoB,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAErE,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC;AAExE,MAAM,MAAM,4BAA4B,GACrC,UAAU,GACV,QAAQ,GACR,aAAa,GACb,aAAa,GACb,sBAAsB,CAAC;AAE1B,MAAM,WAAW,oBAAoB;IACpC,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,kBAAkB;IAClC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG,WAAW,CAAC;AAE5D,MAAM,WAAW,0BAA0B;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,0BAA0B,CAAC;CACvC;AA2BD,qBAAa,mBAAmB;;IAQ/B,YAAY,OAAO,EAAE,0BAA0B,EAG9C;IAED,SAAS,CAAC,QAAQ,EAAE,kBAAkB,EAAE,EAAE,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAG9E;IAED,IAAI,QAAQ,IAAI,oBAAoB,CAEnC;IAED,SAAS,IAAI,MAAM,EAAE,CAEpB;IAED;;;;;OAKG;IACG,WAAW,CAChB,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GACpC,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAezC;IAED,oFAAoF;IACpF,kCAAkC,IAAI,IAAI,CAGzC;IAED;;;;OAIG;IACH,mBAAmB,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAI3D;IAED,gBAAgB,IAAI,4BAA4B,CAY/C;IAED,0EAA0E;IAC1E,MAAM,IAAI,kBAAkB,GAAG,SAAS,CAEvC;CA+BD","sourcesContent":["/**\n * VF.4: the verification lifecycle (spec\n * 2026-09-01-configured-verification-and-formatting.md § 4). The tracker\n * turns bounded executor outcomes into the five completion statuses the\n * spec names — verified, failed, unavailable, interrupted,\n * continued-unverified — and retires a result the moment the workspace it\n * described changes. It never flips failed to passed, and a stale result\n * never presents as current: the conservative rule is that ANY non-empty\n * workspace change retires the record, because proving a change irrelevant\n * is the executor's job, not the lifecycle's.\n *\n * Evidence is bounded by construction: the record carries policy identity,\n * argv, status, duration, exit/signal/truncation flags, and the artifact\n * reference — never raw output. Callers that want output read the artifact\n * through the store's permission gate.\n */\n\nimport type { AuthorizeConfiguredCommand } from \"./permissions/policy-command.ts\";\nimport type { PolicyRunOutcome, PolicyRunStatus } from \"./policy-executor.ts\";\nimport { runPolicyCommand } from \"./policy-executor.ts\";\nimport type { VerificationPolicy } from \"./policy-loader.ts\";\nimport type { WorkspaceArtifactRef } from \"./workspace/artifacts.ts\";\n\nexport type VerificationOutcome = \"verified\" | \"failed\" | \"interrupted\";\n\nexport type VerificationCompletionStatus =\n\t| \"verified\"\n\t| \"failed\"\n\t| \"unavailable\"\n\t| \"interrupted\"\n\t| \"continued-unverified\";\n\nexport interface VerificationEvidence {\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 VerificationRecord {\n\toutcome: VerificationOutcome;\n\tevidence: VerificationEvidence[];\n\tobservedAt: number;\n}\n\nexport type VerificationBoundary = \"explicit\" | \"post-turn\";\n\nexport interface VerificationTrackerOptions {\n\tworkspaceRoot: string;\n\t/**\n\t * PS.1. Absent only in tests and embeddings with no permission authority at\n\t * all; a session that has a gate always supplies one, and a blocked policy\n\t * then spawns nothing.\n\t */\n\tauthorize?: AuthorizeConfiguredCommand;\n}\n\nfunction evidenceFor(outcome: PolicyRunOutcome): VerificationEvidence {\n\treturn {\n\t\tpolicyId: outcome.policyId,\n\t\texecutable: outcome.executable,\n\t\targv: outcome.argv,\n\t\tcwd: outcome.cwd,\n\t\tstatus: outcome.status,\n\t\tdurationMs: outcome.durationMs,\n\t\t...(outcome.exitCode !== undefined ? { exitCode: outcome.exitCode } : {}),\n\t\t...(outcome.signal !== undefined ? { signal: outcome.signal } : {}),\n\t\ttruncated: outcome.truncated,\n\t\t...(outcome.artifact !== undefined ? { artifact: outcome.artifact } : {}),\n\t};\n}\n\nfunction recordOutcome(outcome: PolicyRunOutcome): VerificationRecord {\n\tconst outcomeKind: VerificationOutcome =\n\t\toutcome.status === \"passed\" ? \"verified\" : outcome.status === \"failed\" ? \"failed\" : \"interrupted\";\n\treturn {\n\t\toutcome: outcomeKind,\n\t\tevidence: [evidenceFor(outcome)],\n\t\tobservedAt: Date.now(),\n\t};\n}\n\nexport class VerificationTracker {\n\t#policies: VerificationPolicy[] = [];\n\t#boundary: VerificationBoundary = \"explicit\";\n\t#workspaceRoot: string;\n\t#latest: VerificationRecord | undefined;\n\t#continuedWithoutVerification = false;\n\t#authorize: AuthorizeConfiguredCommand | undefined;\n\n\tconstructor(options: VerificationTrackerOptions) {\n\t\tthis.#workspaceRoot = options.workspaceRoot;\n\t\tthis.#authorize = options.authorize;\n\t}\n\n\tconfigure(policies: VerificationPolicy[], boundary: VerificationBoundary): void {\n\t\tthis.#policies = [...policies];\n\t\tthis.#boundary = boundary;\n\t}\n\n\tget boundary(): VerificationBoundary {\n\t\treturn this.#boundary;\n\t}\n\n\tpolicyIds(): string[] {\n\t\treturn this.#policies.map((policy) => policy.id);\n\t}\n\n\t/**\n\t * Run one configured policy by id (default: the first). Returns the new\n\t * record, or undefined when the id is not configured — surfaced as\n\t * \"unavailable\" by completionStatus(). A run retires any earlier record\n\t * and any earlier continue-without-verification choice.\n\t */\n\tasync runExplicit(\n\t\tpolicyId?: string,\n\t\toptions: { signal?: AbortSignal } = {},\n\t): Promise<VerificationRecord | undefined> {\n\t\tconst policy =\n\t\t\tpolicyId === undefined ? this.#policies[0] : this.#policies.find((candidate) => candidate.id === policyId);\n\t\tif (policy === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tthis.#continuedWithoutVerification = false;\n\t\tconst refusal = await this.#refuseUnauthorized(policy);\n\t\tif (refusal !== undefined) {\n\t\t\tthis.#latest = refusal;\n\t\t\treturn this.#latest;\n\t\t}\n\t\tconst outcome = await runPolicyCommand(policy, { workspaceRoot: this.#workspaceRoot, signal: options.signal });\n\t\tthis.#latest = recordOutcome(outcome);\n\t\treturn this.#latest;\n\t}\n\n\t/** The user chose to finish without verification. Its own status, not a failure. */\n\trecordContinuedWithoutVerification(): void {\n\t\tthis.#latest = undefined;\n\t\tthis.#continuedWithoutVerification = true;\n\t}\n\n\t/**\n\t * Report a workspace change observed after the latest record. Any\n\t * non-empty change set retires the record: the result described a\n\t * workspace that no longer exists.\n\t */\n\tnoteWorkspaceChange(changedPaths: ReadonlySet<string>): void {\n\t\tif (changedPaths.size > 0) {\n\t\t\tthis.#latest = undefined;\n\t\t}\n\t}\n\n\tcompletionStatus(): VerificationCompletionStatus {\n\t\tif (this.#continuedWithoutVerification) {\n\t\t\treturn \"continued-unverified\";\n\t\t}\n\t\tif (this.#latest === undefined) {\n\t\t\treturn \"unavailable\";\n\t\t}\n\t\treturn this.#latest.outcome === \"verified\"\n\t\t\t? \"verified\"\n\t\t\t: this.#latest.outcome === \"failed\"\n\t\t\t\t? \"failed\"\n\t\t\t\t: \"interrupted\";\n\t}\n\n\t/** The current record, if one is live. Stale results are not returned. */\n\tlatest(): VerificationRecord | undefined {\n\t\treturn this.#latest;\n\t}\n\n\t/** A record for a policy the permission authority blocked. Nothing was spawned, so it can never read as verified. */\n\tasync #refuseUnauthorized(policy: VerificationPolicy): Promise<VerificationRecord | undefined> {\n\t\tif (this.#authorize === undefined) return undefined;\n\t\tconst decision = await this.#authorize({\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: this.#workspaceRoot,\n\t\t\twriteScope: undefined,\n\t\t\tcapabilities: new Set([\"exec\"]),\n\t\t\tpermission: policy.permission,\n\t\t});\n\t\tif (!decision.block) return undefined;\n\t\treturn {\n\t\t\toutcome: \"interrupted\",\n\t\t\tevidence: [\n\t\t\t\t{\n\t\t\t\t\tpolicyId: policy.id,\n\t\t\t\t\texecutable: policy.executable,\n\t\t\t\t\targv: policy.argv,\n\t\t\t\t\tcwd: this.#workspaceRoot,\n\t\t\t\t\tstatus: \"refused\",\n\t\t\t\t\tdurationMs: 0,\n\t\t\t\t\ttruncated: false,\n\t\t\t\t},\n\t\t\t],\n\t\t\tobservedAt: Date.now(),\n\t\t};\n\t}\n}\n"]}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VF.4: the verification lifecycle (spec
|
|
3
|
+
* 2026-09-01-configured-verification-and-formatting.md § 4). The tracker
|
|
4
|
+
* turns bounded executor outcomes into the five completion statuses the
|
|
5
|
+
* spec names — verified, failed, unavailable, interrupted,
|
|
6
|
+
* continued-unverified — and retires a result the moment the workspace it
|
|
7
|
+
* described changes. It never flips failed to passed, and a stale result
|
|
8
|
+
* never presents as current: the conservative rule is that ANY non-empty
|
|
9
|
+
* workspace change retires the record, because proving a change irrelevant
|
|
10
|
+
* is the executor's job, not the lifecycle's.
|
|
11
|
+
*
|
|
12
|
+
* Evidence is bounded by construction: the record carries policy identity,
|
|
13
|
+
* argv, status, duration, exit/signal/truncation flags, and the artifact
|
|
14
|
+
* reference — never raw output. Callers that want output read the artifact
|
|
15
|
+
* through the store's permission gate.
|
|
16
|
+
*/
|
|
17
|
+
import { runPolicyCommand } from "./policy-executor.js";
|
|
18
|
+
function evidenceFor(outcome) {
|
|
19
|
+
return {
|
|
20
|
+
policyId: outcome.policyId,
|
|
21
|
+
executable: outcome.executable,
|
|
22
|
+
argv: outcome.argv,
|
|
23
|
+
cwd: outcome.cwd,
|
|
24
|
+
status: outcome.status,
|
|
25
|
+
durationMs: outcome.durationMs,
|
|
26
|
+
...(outcome.exitCode !== undefined ? { exitCode: outcome.exitCode } : {}),
|
|
27
|
+
...(outcome.signal !== undefined ? { signal: outcome.signal } : {}),
|
|
28
|
+
truncated: outcome.truncated,
|
|
29
|
+
...(outcome.artifact !== undefined ? { artifact: outcome.artifact } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function recordOutcome(outcome) {
|
|
33
|
+
const outcomeKind = outcome.status === "passed" ? "verified" : outcome.status === "failed" ? "failed" : "interrupted";
|
|
34
|
+
return {
|
|
35
|
+
outcome: outcomeKind,
|
|
36
|
+
evidence: [evidenceFor(outcome)],
|
|
37
|
+
observedAt: Date.now(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export class VerificationTracker {
|
|
41
|
+
#policies = [];
|
|
42
|
+
#boundary = "explicit";
|
|
43
|
+
#workspaceRoot;
|
|
44
|
+
#latest;
|
|
45
|
+
#continuedWithoutVerification = false;
|
|
46
|
+
#authorize;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.#workspaceRoot = options.workspaceRoot;
|
|
49
|
+
this.#authorize = options.authorize;
|
|
50
|
+
}
|
|
51
|
+
configure(policies, boundary) {
|
|
52
|
+
this.#policies = [...policies];
|
|
53
|
+
this.#boundary = boundary;
|
|
54
|
+
}
|
|
55
|
+
get boundary() {
|
|
56
|
+
return this.#boundary;
|
|
57
|
+
}
|
|
58
|
+
policyIds() {
|
|
59
|
+
return this.#policies.map((policy) => policy.id);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run one configured policy by id (default: the first). Returns the new
|
|
63
|
+
* record, or undefined when the id is not configured — surfaced as
|
|
64
|
+
* "unavailable" by completionStatus(). A run retires any earlier record
|
|
65
|
+
* and any earlier continue-without-verification choice.
|
|
66
|
+
*/
|
|
67
|
+
async runExplicit(policyId, options = {}) {
|
|
68
|
+
const policy = policyId === undefined ? this.#policies[0] : this.#policies.find((candidate) => candidate.id === policyId);
|
|
69
|
+
if (policy === undefined) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
this.#continuedWithoutVerification = false;
|
|
73
|
+
const refusal = await this.#refuseUnauthorized(policy);
|
|
74
|
+
if (refusal !== undefined) {
|
|
75
|
+
this.#latest = refusal;
|
|
76
|
+
return this.#latest;
|
|
77
|
+
}
|
|
78
|
+
const outcome = await runPolicyCommand(policy, { workspaceRoot: this.#workspaceRoot, signal: options.signal });
|
|
79
|
+
this.#latest = recordOutcome(outcome);
|
|
80
|
+
return this.#latest;
|
|
81
|
+
}
|
|
82
|
+
/** The user chose to finish without verification. Its own status, not a failure. */
|
|
83
|
+
recordContinuedWithoutVerification() {
|
|
84
|
+
this.#latest = undefined;
|
|
85
|
+
this.#continuedWithoutVerification = true;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Report a workspace change observed after the latest record. Any
|
|
89
|
+
* non-empty change set retires the record: the result described a
|
|
90
|
+
* workspace that no longer exists.
|
|
91
|
+
*/
|
|
92
|
+
noteWorkspaceChange(changedPaths) {
|
|
93
|
+
if (changedPaths.size > 0) {
|
|
94
|
+
this.#latest = undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
completionStatus() {
|
|
98
|
+
if (this.#continuedWithoutVerification) {
|
|
99
|
+
return "continued-unverified";
|
|
100
|
+
}
|
|
101
|
+
if (this.#latest === undefined) {
|
|
102
|
+
return "unavailable";
|
|
103
|
+
}
|
|
104
|
+
return this.#latest.outcome === "verified"
|
|
105
|
+
? "verified"
|
|
106
|
+
: this.#latest.outcome === "failed"
|
|
107
|
+
? "failed"
|
|
108
|
+
: "interrupted";
|
|
109
|
+
}
|
|
110
|
+
/** The current record, if one is live. Stale results are not returned. */
|
|
111
|
+
latest() {
|
|
112
|
+
return this.#latest;
|
|
113
|
+
}
|
|
114
|
+
/** A record for a policy the permission authority blocked. Nothing was spawned, so it can never read as verified. */
|
|
115
|
+
async #refuseUnauthorized(policy) {
|
|
116
|
+
if (this.#authorize === undefined)
|
|
117
|
+
return undefined;
|
|
118
|
+
const decision = await this.#authorize({
|
|
119
|
+
policyId: policy.id,
|
|
120
|
+
executable: policy.executable,
|
|
121
|
+
argv: policy.argv,
|
|
122
|
+
cwd: this.#workspaceRoot,
|
|
123
|
+
writeScope: undefined,
|
|
124
|
+
capabilities: new Set(["exec"]),
|
|
125
|
+
permission: policy.permission,
|
|
126
|
+
});
|
|
127
|
+
if (!decision.block)
|
|
128
|
+
return undefined;
|
|
129
|
+
return {
|
|
130
|
+
outcome: "interrupted",
|
|
131
|
+
evidence: [
|
|
132
|
+
{
|
|
133
|
+
policyId: policy.id,
|
|
134
|
+
executable: policy.executable,
|
|
135
|
+
argv: policy.argv,
|
|
136
|
+
cwd: this.#workspaceRoot,
|
|
137
|
+
status: "refused",
|
|
138
|
+
durationMs: 0,
|
|
139
|
+
truncated: false,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
observedAt: Date.now(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=verification-lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verification-lifecycle.js","sourceRoot":"","sources":["../../src/core/verification-lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * VF.4: the verification lifecycle (spec\n * 2026-09-01-configured-verification-and-formatting.md § 4). The tracker\n * turns bounded executor outcomes into the five completion statuses the\n * spec names — verified, failed, unavailable, interrupted,\n * continued-unverified — and retires a result the moment the workspace it\n * described changes. It never flips failed to passed, and a stale result\n * never presents as current: the conservative rule is that ANY non-empty\n * workspace change retires the record, because proving a change irrelevant\n * is the executor's job, not the lifecycle's.\n *\n * Evidence is bounded by construction: the record carries policy identity,\n * argv, status, duration, exit/signal/truncation flags, and the artifact\n * reference — never raw output. Callers that want output read the artifact\n * through the store's permission gate.\n */\n\nimport type { AuthorizeConfiguredCommand } from \"./permissions/policy-command.ts\";\nimport type { PolicyRunOutcome, PolicyRunStatus } from \"./policy-executor.ts\";\nimport { runPolicyCommand } from \"./policy-executor.ts\";\nimport type { VerificationPolicy } from \"./policy-loader.ts\";\nimport type { WorkspaceArtifactRef } from \"./workspace/artifacts.ts\";\n\nexport type VerificationOutcome = \"verified\" | \"failed\" | \"interrupted\";\n\nexport type VerificationCompletionStatus =\n\t| \"verified\"\n\t| \"failed\"\n\t| \"unavailable\"\n\t| \"interrupted\"\n\t| \"continued-unverified\";\n\nexport interface VerificationEvidence {\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 VerificationRecord {\n\toutcome: VerificationOutcome;\n\tevidence: VerificationEvidence[];\n\tobservedAt: number;\n}\n\nexport type VerificationBoundary = \"explicit\" | \"post-turn\";\n\nexport interface VerificationTrackerOptions {\n\tworkspaceRoot: string;\n\t/**\n\t * PS.1. Absent only in tests and embeddings with no permission authority at\n\t * all; a session that has a gate always supplies one, and a blocked policy\n\t * then spawns nothing.\n\t */\n\tauthorize?: AuthorizeConfiguredCommand;\n}\n\nfunction evidenceFor(outcome: PolicyRunOutcome): VerificationEvidence {\n\treturn {\n\t\tpolicyId: outcome.policyId,\n\t\texecutable: outcome.executable,\n\t\targv: outcome.argv,\n\t\tcwd: outcome.cwd,\n\t\tstatus: outcome.status,\n\t\tdurationMs: outcome.durationMs,\n\t\t...(outcome.exitCode !== undefined ? { exitCode: outcome.exitCode } : {}),\n\t\t...(outcome.signal !== undefined ? { signal: outcome.signal } : {}),\n\t\ttruncated: outcome.truncated,\n\t\t...(outcome.artifact !== undefined ? { artifact: outcome.artifact } : {}),\n\t};\n}\n\nfunction recordOutcome(outcome: PolicyRunOutcome): VerificationRecord {\n\tconst outcomeKind: VerificationOutcome =\n\t\toutcome.status === \"passed\" ? \"verified\" : outcome.status === \"failed\" ? \"failed\" : \"interrupted\";\n\treturn {\n\t\toutcome: outcomeKind,\n\t\tevidence: [evidenceFor(outcome)],\n\t\tobservedAt: Date.now(),\n\t};\n}\n\nexport class VerificationTracker {\n\t#policies: VerificationPolicy[] = [];\n\t#boundary: VerificationBoundary = \"explicit\";\n\t#workspaceRoot: string;\n\t#latest: VerificationRecord | undefined;\n\t#continuedWithoutVerification = false;\n\t#authorize: AuthorizeConfiguredCommand | undefined;\n\n\tconstructor(options: VerificationTrackerOptions) {\n\t\tthis.#workspaceRoot = options.workspaceRoot;\n\t\tthis.#authorize = options.authorize;\n\t}\n\n\tconfigure(policies: VerificationPolicy[], boundary: VerificationBoundary): void {\n\t\tthis.#policies = [...policies];\n\t\tthis.#boundary = boundary;\n\t}\n\n\tget boundary(): VerificationBoundary {\n\t\treturn this.#boundary;\n\t}\n\n\tpolicyIds(): string[] {\n\t\treturn this.#policies.map((policy) => policy.id);\n\t}\n\n\t/**\n\t * Run one configured policy by id (default: the first). Returns the new\n\t * record, or undefined when the id is not configured — surfaced as\n\t * \"unavailable\" by completionStatus(). A run retires any earlier record\n\t * and any earlier continue-without-verification choice.\n\t */\n\tasync runExplicit(\n\t\tpolicyId?: string,\n\t\toptions: { signal?: AbortSignal } = {},\n\t): Promise<VerificationRecord | undefined> {\n\t\tconst policy =\n\t\t\tpolicyId === undefined ? this.#policies[0] : this.#policies.find((candidate) => candidate.id === policyId);\n\t\tif (policy === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tthis.#continuedWithoutVerification = false;\n\t\tconst refusal = await this.#refuseUnauthorized(policy);\n\t\tif (refusal !== undefined) {\n\t\t\tthis.#latest = refusal;\n\t\t\treturn this.#latest;\n\t\t}\n\t\tconst outcome = await runPolicyCommand(policy, { workspaceRoot: this.#workspaceRoot, signal: options.signal });\n\t\tthis.#latest = recordOutcome(outcome);\n\t\treturn this.#latest;\n\t}\n\n\t/** The user chose to finish without verification. Its own status, not a failure. */\n\trecordContinuedWithoutVerification(): void {\n\t\tthis.#latest = undefined;\n\t\tthis.#continuedWithoutVerification = true;\n\t}\n\n\t/**\n\t * Report a workspace change observed after the latest record. Any\n\t * non-empty change set retires the record: the result described a\n\t * workspace that no longer exists.\n\t */\n\tnoteWorkspaceChange(changedPaths: ReadonlySet<string>): void {\n\t\tif (changedPaths.size > 0) {\n\t\t\tthis.#latest = undefined;\n\t\t}\n\t}\n\n\tcompletionStatus(): VerificationCompletionStatus {\n\t\tif (this.#continuedWithoutVerification) {\n\t\t\treturn \"continued-unverified\";\n\t\t}\n\t\tif (this.#latest === undefined) {\n\t\t\treturn \"unavailable\";\n\t\t}\n\t\treturn this.#latest.outcome === \"verified\"\n\t\t\t? \"verified\"\n\t\t\t: this.#latest.outcome === \"failed\"\n\t\t\t\t? \"failed\"\n\t\t\t\t: \"interrupted\";\n\t}\n\n\t/** The current record, if one is live. Stale results are not returned. */\n\tlatest(): VerificationRecord | undefined {\n\t\treturn this.#latest;\n\t}\n\n\t/** A record for a policy the permission authority blocked. Nothing was spawned, so it can never read as verified. */\n\tasync #refuseUnauthorized(policy: VerificationPolicy): Promise<VerificationRecord | undefined> {\n\t\tif (this.#authorize === undefined) return undefined;\n\t\tconst decision = await this.#authorize({\n\t\t\tpolicyId: policy.id,\n\t\t\texecutable: policy.executable,\n\t\t\targv: policy.argv,\n\t\t\tcwd: this.#workspaceRoot,\n\t\t\twriteScope: undefined,\n\t\t\tcapabilities: new Set([\"exec\"]),\n\t\t\tpermission: policy.permission,\n\t\t});\n\t\tif (!decision.block) return undefined;\n\t\treturn {\n\t\t\toutcome: \"interrupted\",\n\t\t\tevidence: [\n\t\t\t\t{\n\t\t\t\t\tpolicyId: policy.id,\n\t\t\t\t\texecutable: policy.executable,\n\t\t\t\t\targv: policy.argv,\n\t\t\t\t\tcwd: this.#workspaceRoot,\n\t\t\t\t\tstatus: \"refused\",\n\t\t\t\t\tdurationMs: 0,\n\t\t\t\t\ttruncated: false,\n\t\t\t\t},\n\t\t\t],\n\t\t\tobservedAt: Date.now(),\n\t\t};\n\t}\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface WorkspaceArtifactRef {
|
|
2
|
+
artifactId: string;
|
|
3
|
+
/** File name inside the store directory; never a path with separators. */
|
|
4
|
+
file: string;
|
|
5
|
+
sha256: string;
|
|
6
|
+
bytes: number;
|
|
7
|
+
}
|
|
8
|
+
export type WorkspaceArtifactPermission = {
|
|
9
|
+
allowed: true;
|
|
10
|
+
} | {
|
|
11
|
+
allowed: false;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
14
|
+
export type WorkspaceArtifactRead = {
|
|
15
|
+
ok: true;
|
|
16
|
+
bytes: Buffer;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: "denied" | "missing" | "integrity" | "invalid-ref";
|
|
20
|
+
};
|
|
21
|
+
export interface WriteArtifactOptions {
|
|
22
|
+
/** Reject payloads larger than this (default: settings default). */
|
|
23
|
+
maxBytes?: number;
|
|
24
|
+
/** Retention count cap applied after the write (default: settings default). */
|
|
25
|
+
maxArtifacts?: number;
|
|
26
|
+
/** Retention total-bytes cap applied after the write (default: settings default). */
|
|
27
|
+
maxTotalBytes?: number;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
}
|
|
30
|
+
export interface StoredArtifactInfo {
|
|
31
|
+
file: string;
|
|
32
|
+
bytes: number;
|
|
33
|
+
}
|
|
34
|
+
/** `<dir>/<session-basename>.artifacts/workspace-state` next to a session file. */
|
|
35
|
+
export declare function workspaceArtifactDirFor(sessionFile: string): string;
|
|
36
|
+
export declare class WorkspaceArtifactStore {
|
|
37
|
+
#private;
|
|
38
|
+
readonly dir: string;
|
|
39
|
+
constructor(sessionFile: string);
|
|
40
|
+
writeArtifact(bytes: Buffer, options?: WriteArtifactOptions): Promise<WorkspaceArtifactRef>;
|
|
41
|
+
readArtifact(ref: WorkspaceArtifactRef, permission: WorkspaceArtifactPermission): Promise<WorkspaceArtifactRead>;
|
|
42
|
+
listArtifactsSync(): StoredArtifactInfo[];
|
|
43
|
+
removeAll(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=artifacts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/artifacts.ts"],"names":[],"mappings":"AAiDA,MAAM,WAAW,oBAAoB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,2BAA2B,GACpC;IACA,OAAO,EAAE,IAAI,CAAC;CACb,GACD;IACA,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACd,CAAC;AAEL,MAAM,MAAM,qBAAqB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,aAAa,CAAA;CAAE,CAAC;AAE7E,MAAM,WAAW,oBAAoB;IACpC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACd;AAED,mFAAmF;AACnF,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAGnE;AAMD,qBAAa,sBAAsB;;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAGrB,YAAY,WAAW,EAAE,MAAM,EAE9B;IAsDK,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkChG;IAEK,YAAY,CACjB,GAAG,EAAE,oBAAoB,EACzB,UAAU,EAAE,2BAA2B,GACrC,OAAO,CAAC,qBAAqB,CAAC,CAqBhC;IAED,iBAAiB,IAAI,kBAAkB,EAAE,CAExC;IAEK,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/B;CACD","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport {\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport {\n\tDEFAULT_WORKSPACE_MAX_ARTIFACTS,\n\tDEFAULT_WORKSPACE_MAX_PATCH_BYTES,\n\tDEFAULT_WORKSPACE_MAX_TOTAL_BYTES,\n} from \"../settings-manager.ts\";\n\n/**\n * Durable workspace artifact store (WS.3, spec\n * 2026-09-01-harness-correctness-and-workspace-state.md § 2).\n *\n * This is the named owner for workspace-state artifacts. It is scoped to one\n * session file (`<basename>.artifacts/workspace-state/` next to the session\n * JSONL) and guarantees:\n *\n * - atomic publish (unique temp file in the target directory, `rename(2)`,\n * hash verified after publish; interrupted writes leave only `.tmp` files\n * that are ignored and cleaned up by the next write);\n * - restrictive permissions (directory 0o700, files 0o600 on platforms that\n * enforce them);\n * - integrity (sha256 + byte length in the reference; a mismatch reads as\n * `integrity` failure, never as bytes);\n * - bounded retention (FIFO by count and by total bytes; `.tmp` leftovers do\n * not count);\n * - permission-gated retrieval (the caller must pass an explicit permission\n * decision; a denied read never probes the filesystem, so denial leaks no\n * existence oracle).\n *\n * Artifact references confer no permission by themselves: content reaches a\n * provider or a tool only through the normal permission system and output\n * bounds at the consuming surface. Patch bytes never enter the session JSONL\n * or the evidence ledger.\n */\n\nconst FILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\nconst TMP_SUFFIX = \".tmp\";\n\nexport interface WorkspaceArtifactRef {\n\tartifactId: string;\n\t/** File name inside the store directory; never a path with separators. */\n\tfile: string;\n\tsha256: string;\n\tbytes: number;\n}\n\nexport type WorkspaceArtifactPermission =\n\t| {\n\t\t\tallowed: true;\n\t }\n\t| {\n\t\t\tallowed: false;\n\t\t\treason: string;\n\t };\n\nexport type WorkspaceArtifactRead =\n\t| { ok: true; bytes: Buffer }\n\t| { ok: false; reason: \"denied\" | \"missing\" | \"integrity\" | \"invalid-ref\" };\n\nexport interface WriteArtifactOptions {\n\t/** Reject payloads larger than this (default: settings default). */\n\tmaxBytes?: number;\n\t/** Retention count cap applied after the write (default: settings default). */\n\tmaxArtifacts?: number;\n\t/** Retention total-bytes cap applied after the write (default: settings default). */\n\tmaxTotalBytes?: number;\n\tsignal?: AbortSignal;\n}\n\nexport interface StoredArtifactInfo {\n\tfile: string;\n\tbytes: number;\n}\n\n/** `<dir>/<session-basename>.artifacts/workspace-state` next to a session file. */\nexport function workspaceArtifactDirFor(sessionFile: string): string {\n\tconst base = basename(sessionFile).replace(/\\.jsonl$/, \"\");\n\treturn join(dirname(sessionFile), `${base}.artifacts`, \"workspace-state\");\n}\n\nfunction sha256Hex(bytes: Buffer): string {\n\treturn `sha256:${createHash(\"sha256\").update(bytes).digest(\"hex\")}`;\n}\n\nexport class WorkspaceArtifactStore {\n\treadonly dir: string;\n\t#counter = 0;\n\n\tconstructor(sessionFile: string) {\n\t\tthis.dir = workspaceArtifactDirFor(sessionFile);\n\t}\n\n\t#validFileName(file: string): boolean {\n\t\treturn FILE_PATTERN.test(file) && !file.endsWith(TMP_SUFFIX) && !file.includes(\"..\");\n\t}\n\n\t#listStored(): StoredArtifactInfo[] {\n\t\tif (!existsSync(this.dir)) return [];\n\t\tconst stored: StoredArtifactInfo[] = [];\n\t\tfor (const file of readdirSync(this.dir)) {\n\t\t\tif (!this.#validFileName(file)) continue;\n\t\t\tconst full = join(this.dir, file);\n\t\t\tconst stat = statSync(full);\n\t\t\tif (!stat.isFile()) continue;\n\t\t\tstored.push({ file, bytes: stat.size });\n\t\t}\n\t\t// File names embed a sortable write sequence\n\t\t// (`<time>-<counter>-<artifactId>.patch`), so lexicographic order is\n\t\t// chronological regardless of filesystem timestamp granularity.\n\t\tstored.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));\n\t\treturn stored;\n\t}\n\n\t#removeStaleTemp(): void {\n\t\tif (!existsSync(this.dir)) return;\n\t\tfor (const file of readdirSync(this.dir)) {\n\t\t\tif (file.endsWith(TMP_SUFFIX)) {\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(join(this.dir, file));\n\t\t\t\t} catch {\n\t\t\t\t\t// A concurrent writer owns it; leave it for the next pass.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t#prune(maxArtifacts: number, maxTotalBytes: number): void {\n\t\tconst stored = this.#listStored();\n\t\tlet total = stored.reduce((sum, info) => sum + info.bytes, 0);\n\t\tlet excessCount = stored.length - maxArtifacts;\n\t\tfor (const info of stored) {\n\t\t\tif (excessCount <= 0 && total <= maxTotalBytes) break;\n\t\t\tif (excessCount > 0 || total > maxTotalBytes) {\n\t\t\t\ttry {\n\t\t\t\t\trmSync(join(this.dir, info.file), { force: true });\n\t\t\t\t\ttotal -= info.bytes;\n\t\t\t\t\texcessCount--;\n\t\t\t\t} catch {\n\t\t\t\t\t// Unlinkable now; the next write retries.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tasync writeArtifact(bytes: Buffer, options?: WriteArtifactOptions): Promise<WorkspaceArtifactRef> {\n\t\tif (options?.signal?.aborted) {\n\t\t\tthrow new Error(\"workspace artifact write cancelled\");\n\t\t}\n\t\tconst maxBytes = options?.maxBytes ?? DEFAULT_WORKSPACE_MAX_PATCH_BYTES;\n\t\tif (bytes.byteLength > maxBytes) {\n\t\t\tthrow new Error(`workspace artifact exceeds the configured cap (${bytes.byteLength} > ${maxBytes} bytes)`);\n\t\t}\n\t\tconst maxArtifacts = options?.maxArtifacts ?? DEFAULT_WORKSPACE_MAX_ARTIFACTS;\n\t\tconst maxTotalBytes = options?.maxTotalBytes ?? DEFAULT_WORKSPACE_MAX_TOTAL_BYTES;\n\n\t\tmkdirSync(this.dir, { recursive: true, mode: 0o700 });\n\t\tthis.#removeStaleTemp();\n\n\t\tconst artifactId = randomUUID();\n\t\tconst sequence = `${Date.now().toString(36).padStart(8, \"0\")}-${(this.#counter++).toString(36).padStart(4, \"0\")}`;\n\t\tconst file = `${sequence}-${artifactId}.patch`;\n\t\tconst finalPath = join(this.dir, file);\n\t\tconst tempPath = join(this.dir, `${artifactId}${TMP_SUFFIX}`);\n\t\tconst expected = sha256Hex(bytes);\n\t\twriteFileSync(tempPath, bytes, { mode: 0o600 });\n\t\t// Atomic publish: readers only ever see the final name.\n\t\trenameSync(tempPath, finalPath);\n\t\t// Integrity check on our own publish path: a silent corruption or a\n\t\t// truncating filesystem must not leave a reference that reads back\n\t\t// different bytes.\n\t\tconst written = readFileSync(finalPath);\n\t\tif (sha256Hex(written) !== expected || written.byteLength !== bytes.byteLength) {\n\t\t\trmSync(finalPath, { force: true });\n\t\t\tthrow new Error(\"workspace artifact failed its post-publish integrity check\");\n\t\t}\n\n\t\tthis.#prune(maxArtifacts, maxTotalBytes);\n\t\treturn { artifactId, file, sha256: expected, bytes: bytes.byteLength };\n\t}\n\n\tasync readArtifact(\n\t\tref: WorkspaceArtifactRef,\n\t\tpermission: WorkspaceArtifactPermission,\n\t): Promise<WorkspaceArtifactRead> {\n\t\t// Permission is checked before any filesystem access so denial cannot\n\t\t// leak existence through a missing-vs-present distinction.\n\t\tif (!permission.allowed) {\n\t\t\treturn { ok: false, reason: \"denied\" };\n\t\t}\n\t\tif (typeof ref?.file !== \"string\" || !this.#validFileName(ref.file)) {\n\t\t\treturn { ok: false, reason: \"invalid-ref\" };\n\t\t}\n\t\tconst full = join(this.dir, ref.file);\n\t\tlet bytes: Buffer;\n\t\ttry {\n\t\t\tif (!existsSync(full)) return { ok: false, reason: \"missing\" };\n\t\t\tbytes = readFileSync(full);\n\t\t} catch {\n\t\t\treturn { ok: false, reason: \"missing\" };\n\t\t}\n\t\tif (bytes.byteLength !== ref.bytes || sha256Hex(bytes) !== ref.sha256) {\n\t\t\treturn { ok: false, reason: \"integrity\" };\n\t\t}\n\t\treturn { ok: true, bytes };\n\t}\n\n\tlistArtifactsSync(): StoredArtifactInfo[] {\n\t\treturn this.#listStored();\n\t}\n\n\tasync removeAll(): Promise<void> {\n\t\trmSync(this.dir, { recursive: true, force: true });\n\t}\n}\n"]}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { DEFAULT_WORKSPACE_MAX_ARTIFACTS, DEFAULT_WORKSPACE_MAX_PATCH_BYTES, DEFAULT_WORKSPACE_MAX_TOTAL_BYTES, } from "../settings-manager.js";
|
|
5
|
+
/**
|
|
6
|
+
* Durable workspace artifact store (WS.3, spec
|
|
7
|
+
* 2026-09-01-harness-correctness-and-workspace-state.md § 2).
|
|
8
|
+
*
|
|
9
|
+
* This is the named owner for workspace-state artifacts. It is scoped to one
|
|
10
|
+
* session file (`<basename>.artifacts/workspace-state/` next to the session
|
|
11
|
+
* JSONL) and guarantees:
|
|
12
|
+
*
|
|
13
|
+
* - atomic publish (unique temp file in the target directory, `rename(2)`,
|
|
14
|
+
* hash verified after publish; interrupted writes leave only `.tmp` files
|
|
15
|
+
* that are ignored and cleaned up by the next write);
|
|
16
|
+
* - restrictive permissions (directory 0o700, files 0o600 on platforms that
|
|
17
|
+
* enforce them);
|
|
18
|
+
* - integrity (sha256 + byte length in the reference; a mismatch reads as
|
|
19
|
+
* `integrity` failure, never as bytes);
|
|
20
|
+
* - bounded retention (FIFO by count and by total bytes; `.tmp` leftovers do
|
|
21
|
+
* not count);
|
|
22
|
+
* - permission-gated retrieval (the caller must pass an explicit permission
|
|
23
|
+
* decision; a denied read never probes the filesystem, so denial leaks no
|
|
24
|
+
* existence oracle).
|
|
25
|
+
*
|
|
26
|
+
* Artifact references confer no permission by themselves: content reaches a
|
|
27
|
+
* provider or a tool only through the normal permission system and output
|
|
28
|
+
* bounds at the consuming surface. Patch bytes never enter the session JSONL
|
|
29
|
+
* or the evidence ledger.
|
|
30
|
+
*/
|
|
31
|
+
const FILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
32
|
+
const TMP_SUFFIX = ".tmp";
|
|
33
|
+
/** `<dir>/<session-basename>.artifacts/workspace-state` next to a session file. */
|
|
34
|
+
export function workspaceArtifactDirFor(sessionFile) {
|
|
35
|
+
const base = basename(sessionFile).replace(/\.jsonl$/, "");
|
|
36
|
+
return join(dirname(sessionFile), `${base}.artifacts`, "workspace-state");
|
|
37
|
+
}
|
|
38
|
+
function sha256Hex(bytes) {
|
|
39
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
40
|
+
}
|
|
41
|
+
export class WorkspaceArtifactStore {
|
|
42
|
+
dir;
|
|
43
|
+
#counter = 0;
|
|
44
|
+
constructor(sessionFile) {
|
|
45
|
+
this.dir = workspaceArtifactDirFor(sessionFile);
|
|
46
|
+
}
|
|
47
|
+
#validFileName(file) {
|
|
48
|
+
return FILE_PATTERN.test(file) && !file.endsWith(TMP_SUFFIX) && !file.includes("..");
|
|
49
|
+
}
|
|
50
|
+
#listStored() {
|
|
51
|
+
if (!existsSync(this.dir))
|
|
52
|
+
return [];
|
|
53
|
+
const stored = [];
|
|
54
|
+
for (const file of readdirSync(this.dir)) {
|
|
55
|
+
if (!this.#validFileName(file))
|
|
56
|
+
continue;
|
|
57
|
+
const full = join(this.dir, file);
|
|
58
|
+
const stat = statSync(full);
|
|
59
|
+
if (!stat.isFile())
|
|
60
|
+
continue;
|
|
61
|
+
stored.push({ file, bytes: stat.size });
|
|
62
|
+
}
|
|
63
|
+
// File names embed a sortable write sequence
|
|
64
|
+
// (`<time>-<counter>-<artifactId>.patch`), so lexicographic order is
|
|
65
|
+
// chronological regardless of filesystem timestamp granularity.
|
|
66
|
+
stored.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
67
|
+
return stored;
|
|
68
|
+
}
|
|
69
|
+
#removeStaleTemp() {
|
|
70
|
+
if (!existsSync(this.dir))
|
|
71
|
+
return;
|
|
72
|
+
for (const file of readdirSync(this.dir)) {
|
|
73
|
+
if (file.endsWith(TMP_SUFFIX)) {
|
|
74
|
+
try {
|
|
75
|
+
unlinkSync(join(this.dir, file));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// A concurrent writer owns it; leave it for the next pass.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
#prune(maxArtifacts, maxTotalBytes) {
|
|
84
|
+
const stored = this.#listStored();
|
|
85
|
+
let total = stored.reduce((sum, info) => sum + info.bytes, 0);
|
|
86
|
+
let excessCount = stored.length - maxArtifacts;
|
|
87
|
+
for (const info of stored) {
|
|
88
|
+
if (excessCount <= 0 && total <= maxTotalBytes)
|
|
89
|
+
break;
|
|
90
|
+
if (excessCount > 0 || total > maxTotalBytes) {
|
|
91
|
+
try {
|
|
92
|
+
rmSync(join(this.dir, info.file), { force: true });
|
|
93
|
+
total -= info.bytes;
|
|
94
|
+
excessCount--;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Unlinkable now; the next write retries.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async writeArtifact(bytes, options) {
|
|
103
|
+
if (options?.signal?.aborted) {
|
|
104
|
+
throw new Error("workspace artifact write cancelled");
|
|
105
|
+
}
|
|
106
|
+
const maxBytes = options?.maxBytes ?? DEFAULT_WORKSPACE_MAX_PATCH_BYTES;
|
|
107
|
+
if (bytes.byteLength > maxBytes) {
|
|
108
|
+
throw new Error(`workspace artifact exceeds the configured cap (${bytes.byteLength} > ${maxBytes} bytes)`);
|
|
109
|
+
}
|
|
110
|
+
const maxArtifacts = options?.maxArtifacts ?? DEFAULT_WORKSPACE_MAX_ARTIFACTS;
|
|
111
|
+
const maxTotalBytes = options?.maxTotalBytes ?? DEFAULT_WORKSPACE_MAX_TOTAL_BYTES;
|
|
112
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
113
|
+
this.#removeStaleTemp();
|
|
114
|
+
const artifactId = randomUUID();
|
|
115
|
+
const sequence = `${Date.now().toString(36).padStart(8, "0")}-${(this.#counter++).toString(36).padStart(4, "0")}`;
|
|
116
|
+
const file = `${sequence}-${artifactId}.patch`;
|
|
117
|
+
const finalPath = join(this.dir, file);
|
|
118
|
+
const tempPath = join(this.dir, `${artifactId}${TMP_SUFFIX}`);
|
|
119
|
+
const expected = sha256Hex(bytes);
|
|
120
|
+
writeFileSync(tempPath, bytes, { mode: 0o600 });
|
|
121
|
+
// Atomic publish: readers only ever see the final name.
|
|
122
|
+
renameSync(tempPath, finalPath);
|
|
123
|
+
// Integrity check on our own publish path: a silent corruption or a
|
|
124
|
+
// truncating filesystem must not leave a reference that reads back
|
|
125
|
+
// different bytes.
|
|
126
|
+
const written = readFileSync(finalPath);
|
|
127
|
+
if (sha256Hex(written) !== expected || written.byteLength !== bytes.byteLength) {
|
|
128
|
+
rmSync(finalPath, { force: true });
|
|
129
|
+
throw new Error("workspace artifact failed its post-publish integrity check");
|
|
130
|
+
}
|
|
131
|
+
this.#prune(maxArtifacts, maxTotalBytes);
|
|
132
|
+
return { artifactId, file, sha256: expected, bytes: bytes.byteLength };
|
|
133
|
+
}
|
|
134
|
+
async readArtifact(ref, permission) {
|
|
135
|
+
// Permission is checked before any filesystem access so denial cannot
|
|
136
|
+
// leak existence through a missing-vs-present distinction.
|
|
137
|
+
if (!permission.allowed) {
|
|
138
|
+
return { ok: false, reason: "denied" };
|
|
139
|
+
}
|
|
140
|
+
if (typeof ref?.file !== "string" || !this.#validFileName(ref.file)) {
|
|
141
|
+
return { ok: false, reason: "invalid-ref" };
|
|
142
|
+
}
|
|
143
|
+
const full = join(this.dir, ref.file);
|
|
144
|
+
let bytes;
|
|
145
|
+
try {
|
|
146
|
+
if (!existsSync(full))
|
|
147
|
+
return { ok: false, reason: "missing" };
|
|
148
|
+
bytes = readFileSync(full);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return { ok: false, reason: "missing" };
|
|
152
|
+
}
|
|
153
|
+
if (bytes.byteLength !== ref.bytes || sha256Hex(bytes) !== ref.sha256) {
|
|
154
|
+
return { ok: false, reason: "integrity" };
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, bytes };
|
|
157
|
+
}
|
|
158
|
+
listArtifactsSync() {
|
|
159
|
+
return this.#listStored();
|
|
160
|
+
}
|
|
161
|
+
async removeAll() {
|
|
162
|
+
rmSync(this.dir, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=artifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../../../src/core/workspace/artifacts.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport {\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport {\n\tDEFAULT_WORKSPACE_MAX_ARTIFACTS,\n\tDEFAULT_WORKSPACE_MAX_PATCH_BYTES,\n\tDEFAULT_WORKSPACE_MAX_TOTAL_BYTES,\n} from \"../settings-manager.ts\";\n\n/**\n * Durable workspace artifact store (WS.3, spec\n * 2026-09-01-harness-correctness-and-workspace-state.md § 2).\n *\n * This is the named owner for workspace-state artifacts. It is scoped to one\n * session file (`<basename>.artifacts/workspace-state/` next to the session\n * JSONL) and guarantees:\n *\n * - atomic publish (unique temp file in the target directory, `rename(2)`,\n * hash verified after publish; interrupted writes leave only `.tmp` files\n * that are ignored and cleaned up by the next write);\n * - restrictive permissions (directory 0o700, files 0o600 on platforms that\n * enforce them);\n * - integrity (sha256 + byte length in the reference; a mismatch reads as\n * `integrity` failure, never as bytes);\n * - bounded retention (FIFO by count and by total bytes; `.tmp` leftovers do\n * not count);\n * - permission-gated retrieval (the caller must pass an explicit permission\n * decision; a denied read never probes the filesystem, so denial leaks no\n * existence oracle).\n *\n * Artifact references confer no permission by themselves: content reaches a\n * provider or a tool only through the normal permission system and output\n * bounds at the consuming surface. Patch bytes never enter the session JSONL\n * or the evidence ledger.\n */\n\nconst FILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\nconst TMP_SUFFIX = \".tmp\";\n\nexport interface WorkspaceArtifactRef {\n\tartifactId: string;\n\t/** File name inside the store directory; never a path with separators. */\n\tfile: string;\n\tsha256: string;\n\tbytes: number;\n}\n\nexport type WorkspaceArtifactPermission =\n\t| {\n\t\t\tallowed: true;\n\t }\n\t| {\n\t\t\tallowed: false;\n\t\t\treason: string;\n\t };\n\nexport type WorkspaceArtifactRead =\n\t| { ok: true; bytes: Buffer }\n\t| { ok: false; reason: \"denied\" | \"missing\" | \"integrity\" | \"invalid-ref\" };\n\nexport interface WriteArtifactOptions {\n\t/** Reject payloads larger than this (default: settings default). */\n\tmaxBytes?: number;\n\t/** Retention count cap applied after the write (default: settings default). */\n\tmaxArtifacts?: number;\n\t/** Retention total-bytes cap applied after the write (default: settings default). */\n\tmaxTotalBytes?: number;\n\tsignal?: AbortSignal;\n}\n\nexport interface StoredArtifactInfo {\n\tfile: string;\n\tbytes: number;\n}\n\n/** `<dir>/<session-basename>.artifacts/workspace-state` next to a session file. */\nexport function workspaceArtifactDirFor(sessionFile: string): string {\n\tconst base = basename(sessionFile).replace(/\\.jsonl$/, \"\");\n\treturn join(dirname(sessionFile), `${base}.artifacts`, \"workspace-state\");\n}\n\nfunction sha256Hex(bytes: Buffer): string {\n\treturn `sha256:${createHash(\"sha256\").update(bytes).digest(\"hex\")}`;\n}\n\nexport class WorkspaceArtifactStore {\n\treadonly dir: string;\n\t#counter = 0;\n\n\tconstructor(sessionFile: string) {\n\t\tthis.dir = workspaceArtifactDirFor(sessionFile);\n\t}\n\n\t#validFileName(file: string): boolean {\n\t\treturn FILE_PATTERN.test(file) && !file.endsWith(TMP_SUFFIX) && !file.includes(\"..\");\n\t}\n\n\t#listStored(): StoredArtifactInfo[] {\n\t\tif (!existsSync(this.dir)) return [];\n\t\tconst stored: StoredArtifactInfo[] = [];\n\t\tfor (const file of readdirSync(this.dir)) {\n\t\t\tif (!this.#validFileName(file)) continue;\n\t\t\tconst full = join(this.dir, file);\n\t\t\tconst stat = statSync(full);\n\t\t\tif (!stat.isFile()) continue;\n\t\t\tstored.push({ file, bytes: stat.size });\n\t\t}\n\t\t// File names embed a sortable write sequence\n\t\t// (`<time>-<counter>-<artifactId>.patch`), so lexicographic order is\n\t\t// chronological regardless of filesystem timestamp granularity.\n\t\tstored.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));\n\t\treturn stored;\n\t}\n\n\t#removeStaleTemp(): void {\n\t\tif (!existsSync(this.dir)) return;\n\t\tfor (const file of readdirSync(this.dir)) {\n\t\t\tif (file.endsWith(TMP_SUFFIX)) {\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(join(this.dir, file));\n\t\t\t\t} catch {\n\t\t\t\t\t// A concurrent writer owns it; leave it for the next pass.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t#prune(maxArtifacts: number, maxTotalBytes: number): void {\n\t\tconst stored = this.#listStored();\n\t\tlet total = stored.reduce((sum, info) => sum + info.bytes, 0);\n\t\tlet excessCount = stored.length - maxArtifacts;\n\t\tfor (const info of stored) {\n\t\t\tif (excessCount <= 0 && total <= maxTotalBytes) break;\n\t\t\tif (excessCount > 0 || total > maxTotalBytes) {\n\t\t\t\ttry {\n\t\t\t\t\trmSync(join(this.dir, info.file), { force: true });\n\t\t\t\t\ttotal -= info.bytes;\n\t\t\t\t\texcessCount--;\n\t\t\t\t} catch {\n\t\t\t\t\t// Unlinkable now; the next write retries.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tasync writeArtifact(bytes: Buffer, options?: WriteArtifactOptions): Promise<WorkspaceArtifactRef> {\n\t\tif (options?.signal?.aborted) {\n\t\t\tthrow new Error(\"workspace artifact write cancelled\");\n\t\t}\n\t\tconst maxBytes = options?.maxBytes ?? DEFAULT_WORKSPACE_MAX_PATCH_BYTES;\n\t\tif (bytes.byteLength > maxBytes) {\n\t\t\tthrow new Error(`workspace artifact exceeds the configured cap (${bytes.byteLength} > ${maxBytes} bytes)`);\n\t\t}\n\t\tconst maxArtifacts = options?.maxArtifacts ?? DEFAULT_WORKSPACE_MAX_ARTIFACTS;\n\t\tconst maxTotalBytes = options?.maxTotalBytes ?? DEFAULT_WORKSPACE_MAX_TOTAL_BYTES;\n\n\t\tmkdirSync(this.dir, { recursive: true, mode: 0o700 });\n\t\tthis.#removeStaleTemp();\n\n\t\tconst artifactId = randomUUID();\n\t\tconst sequence = `${Date.now().toString(36).padStart(8, \"0\")}-${(this.#counter++).toString(36).padStart(4, \"0\")}`;\n\t\tconst file = `${sequence}-${artifactId}.patch`;\n\t\tconst finalPath = join(this.dir, file);\n\t\tconst tempPath = join(this.dir, `${artifactId}${TMP_SUFFIX}`);\n\t\tconst expected = sha256Hex(bytes);\n\t\twriteFileSync(tempPath, bytes, { mode: 0o600 });\n\t\t// Atomic publish: readers only ever see the final name.\n\t\trenameSync(tempPath, finalPath);\n\t\t// Integrity check on our own publish path: a silent corruption or a\n\t\t// truncating filesystem must not leave a reference that reads back\n\t\t// different bytes.\n\t\tconst written = readFileSync(finalPath);\n\t\tif (sha256Hex(written) !== expected || written.byteLength !== bytes.byteLength) {\n\t\t\trmSync(finalPath, { force: true });\n\t\t\tthrow new Error(\"workspace artifact failed its post-publish integrity check\");\n\t\t}\n\n\t\tthis.#prune(maxArtifacts, maxTotalBytes);\n\t\treturn { artifactId, file, sha256: expected, bytes: bytes.byteLength };\n\t}\n\n\tasync readArtifact(\n\t\tref: WorkspaceArtifactRef,\n\t\tpermission: WorkspaceArtifactPermission,\n\t): Promise<WorkspaceArtifactRead> {\n\t\t// Permission is checked before any filesystem access so denial cannot\n\t\t// leak existence through a missing-vs-present distinction.\n\t\tif (!permission.allowed) {\n\t\t\treturn { ok: false, reason: \"denied\" };\n\t\t}\n\t\tif (typeof ref?.file !== \"string\" || !this.#validFileName(ref.file)) {\n\t\t\treturn { ok: false, reason: \"invalid-ref\" };\n\t\t}\n\t\tconst full = join(this.dir, ref.file);\n\t\tlet bytes: Buffer;\n\t\ttry {\n\t\t\tif (!existsSync(full)) return { ok: false, reason: \"missing\" };\n\t\t\tbytes = readFileSync(full);\n\t\t} catch {\n\t\t\treturn { ok: false, reason: \"missing\" };\n\t\t}\n\t\tif (bytes.byteLength !== ref.bytes || sha256Hex(bytes) !== ref.sha256) {\n\t\t\treturn { ok: false, reason: \"integrity\" };\n\t\t}\n\t\treturn { ok: true, bytes };\n\t}\n\n\tlistArtifactsSync(): StoredArtifactInfo[] {\n\t\treturn this.#listStored();\n\t}\n\n\tasync removeAll(): Promise<void> {\n\t\trmSync(this.dir, { recursive: true, force: true });\n\t}\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WorkspaceStateComparison, WorkspaceStateRecord } from "./state.ts";
|
|
2
|
+
import { WORKSPACE_RECORD_VERSION } from "./state.ts";
|
|
3
|
+
/** Upper bound on `changedPaths` carried in one comparison record. */
|
|
4
|
+
export declare const MAX_CHANGED_PATHS = 200;
|
|
5
|
+
export type WorkspaceComparisonOutcome = Pick<WorkspaceStateComparison, "result" | "changedPaths" | "warnings">;
|
|
6
|
+
export interface WorkspaceComparisonOptions {
|
|
7
|
+
/** Probe for stored patch artifacts; defaults to existence on disk. */
|
|
8
|
+
artifactProbe?: (storePath: string) => boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* WS.5: compare fresh workspace state against a stored observation (spec
|
|
12
|
+
* 2026-09-01-harness-correctness-and-workspace-state.md § 3). Pure: same
|
|
13
|
+
* inputs, same outcome. `unavailable` — the workspace could not be observed
|
|
14
|
+
* now; `inconclusive` — no reliable comparison could be established, so no
|
|
15
|
+
* drift claim is made in either direction; drift is never silently hidden:
|
|
16
|
+
* an old patch is never presented as current state.
|
|
17
|
+
*/
|
|
18
|
+
export declare function compareWorkspaceObservations(stored: WorkspaceStateRecord, fresh: WorkspaceStateRecord | undefined, options?: WorkspaceComparisonOptions): WorkspaceComparisonOutcome;
|
|
19
|
+
/** Shape of a persisted comparison record, minus what the caller supplies. */
|
|
20
|
+
export type WorkspaceComparisonFields = WorkspaceComparisonOutcome & {
|
|
21
|
+
comparedAt: string;
|
|
22
|
+
comparedToObservationId: string;
|
|
23
|
+
version: typeof WORKSPACE_RECORD_VERSION;
|
|
24
|
+
};
|
|
25
|
+
/** Build the record handed to appendWorkspaceComparison. */
|
|
26
|
+
export declare function buildWorkspaceComparison(comparedToObservationId: string, outcome: WorkspaceComparisonOutcome, now?: () => string): WorkspaceStateComparison;
|
|
27
|
+
//# sourceMappingURL=comparison.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comparison.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/comparison.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACjF,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAEtD,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAAC,wBAAwB,EAAE,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAC,CAAC;AAEhH,MAAM,WAAW,0BAA0B;IAC1C,uEAAuE;IACvE,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;CAC/C;AAyCD;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC3C,MAAM,EAAE,oBAAoB,EAC5B,KAAK,EAAE,oBAAoB,GAAG,SAAS,EACvC,OAAO,GAAE,0BAA+B,GACtC,0BAA0B,CAyC5B;AAED,8EAA8E;AAC9E,MAAM,MAAM,yBAAyB,GAAG,0BAA0B,GAAG;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB,EAAE,MAAM,CAAC;IAChC,OAAO,EAAE,OAAO,wBAAwB,CAAC;CACzC,CAAC;AAEF,4DAA4D;AAC5D,wBAAgB,wBAAwB,CACvC,uBAAuB,EAAE,MAAM,EAC/B,OAAO,EAAE,0BAA0B,EACnC,GAAG,GAAE,MAAM,MAAuC,GAChD,wBAAwB,CAS1B","sourcesContent":["import { existsSync } from \"node:fs\";\nimport type { WorkspaceStateComparison, WorkspaceStateRecord } from \"./state.ts\";\nimport { WORKSPACE_RECORD_VERSION } from \"./state.ts\";\n\n/** Upper bound on `changedPaths` carried in one comparison record. */\nexport const MAX_CHANGED_PATHS = 200;\n\nexport type WorkspaceComparisonOutcome = Pick<WorkspaceStateComparison, \"result\" | \"changedPaths\" | \"warnings\">;\n\nexport interface WorkspaceComparisonOptions {\n\t/** Probe for stored patch artifacts; defaults to existence on disk. */\n\tartifactProbe?: (storePath: string) => boolean;\n}\n\nfunction pathTupleKey(path: {\n\tpath: string;\n\tkind: string;\n\tstaged?: boolean;\n\tunstaged?: boolean;\n\tignored?: boolean;\n\tcontentHash?: string;\n}): string {\n\treturn [\n\t\tpath.path,\n\t\tpath.kind,\n\t\tpath.staged ? \"s\" : \"\",\n\t\tpath.unstaged ? \"u\" : \"\",\n\t\tpath.ignored ? \"i\" : \"\",\n\t\tpath.contentHash ?? \"\",\n\t].join(\"\\u0000\");\n}\n\nfunction changedPathsBetween(stored: WorkspaceStateRecord, fresh: WorkspaceStateRecord, warnings: string[]): string[] {\n\tconst storedKeys = new Map<string, string>();\n\tfor (const p of stored.paths) storedKeys.set(p.path, pathTupleKey(p));\n\tconst freshKeys = new Map<string, string>();\n\tfor (const p of fresh.paths) freshKeys.set(p.path, pathTupleKey(p));\n\n\tconst changed = new Set<string>();\n\tfor (const [path, key] of storedKeys) {\n\t\tif (freshKeys.get(path) !== key) changed.add(path);\n\t}\n\tfor (const [path, key] of freshKeys) {\n\t\tif (storedKeys.get(path) !== key) changed.add(path);\n\t}\n\tconst sorted = [...changed].sort();\n\tif (sorted.length > MAX_CHANGED_PATHS) {\n\t\twarnings.push(`changed path list truncated at ${MAX_CHANGED_PATHS} of ${sorted.length} entries`);\n\t\treturn sorted.slice(0, MAX_CHANGED_PATHS);\n\t}\n\treturn sorted;\n}\n\n/**\n * WS.5: compare fresh workspace state against a stored observation (spec\n * 2026-09-01-harness-correctness-and-workspace-state.md § 3). Pure: same\n * inputs, same outcome. `unavailable` — the workspace could not be observed\n * now; `inconclusive` — no reliable comparison could be established, so no\n * drift claim is made in either direction; drift is never silently hidden:\n * an old patch is never presented as current state.\n */\nexport function compareWorkspaceObservations(\n\tstored: WorkspaceStateRecord,\n\tfresh: WorkspaceStateRecord | undefined,\n\toptions: WorkspaceComparisonOptions = {},\n): WorkspaceComparisonOutcome {\n\tconst warnings: string[] = [];\n\n\tif (!fresh || fresh.status === \"failed\" || fresh.status === \"unsupported\") {\n\t\treturn { result: \"unavailable\", warnings };\n\t}\n\tif (fresh.status === \"incomplete\") {\n\t\twarnings.push(\"fresh observation is incomplete; comparison is not reliable\");\n\t\treturn { result: \"inconclusive\", warnings };\n\t}\n\tif (stored.status === \"incomplete\") {\n\t\twarnings.push(\"stored observation is incomplete; comparison is not reliable\");\n\t\treturn { result: \"inconclusive\", warnings };\n\t}\n\tif (stored.status !== \"observed\") {\n\t\twarnings.push(\"stored observation has no comparable baseline\");\n\t\treturn { result: \"inconclusive\", warnings };\n\t}\n\n\tconst storedBase = stored.base;\n\tconst freshBase = fresh.base;\n\tif (!storedBase?.worktreeDigest || !freshBase?.worktreeDigest) {\n\t\twarnings.push(\"worktree digest missing on one side; comparison is not reliable\");\n\t\treturn { result: \"inconclusive\", warnings };\n\t}\n\n\tconst sameIndex = (storedBase.indexDigest ?? \"\") === (freshBase.indexDigest ?? \"\");\n\tconst sameWorktree = storedBase.worktreeDigest === freshBase.worktreeDigest;\n\n\tif (stored.patchArtifactRef) {\n\t\tconst probe = options.artifactProbe ?? ((p: string) => existsSync(p));\n\t\tif (!probe(stored.patchArtifactRef.file)) {\n\t\t\twarnings.push(`stored workspace patch artifact ${stored.patchArtifactRef.artifactId} is missing on disk`);\n\t\t}\n\t}\n\n\tif (sameIndex && sameWorktree) {\n\t\treturn { result: \"same\", warnings };\n\t}\n\tconst changedPaths = changedPathsBetween(stored, fresh, warnings);\n\treturn { result: \"drifted\", changedPaths, warnings };\n}\n\n/** Shape of a persisted comparison record, minus what the caller supplies. */\nexport type WorkspaceComparisonFields = WorkspaceComparisonOutcome & {\n\tcomparedAt: string;\n\tcomparedToObservationId: string;\n\tversion: typeof WORKSPACE_RECORD_VERSION;\n};\n\n/** Build the record handed to appendWorkspaceComparison. */\nexport function buildWorkspaceComparison(\n\tcomparedToObservationId: string,\n\toutcome: WorkspaceComparisonOutcome,\n\tnow: () => string = () => new Date().toISOString(),\n): WorkspaceStateComparison {\n\treturn {\n\t\tversion: WORKSPACE_RECORD_VERSION,\n\t\tcomparedAt: now(),\n\t\tcomparedToObservationId,\n\t\tresult: outcome.result,\n\t\tchangedPaths: outcome.changedPaths,\n\t\twarnings: outcome.warnings,\n\t};\n}\n"]}
|