@sayknow-cli/coding-agent 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +9 -1
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-contract.d.ts +40 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +156 -10
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +33 -2
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +5 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +13 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
- package/dist/types/runtime-mcp/manager.d.ts +9 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +52 -4
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/query/handlers.d.ts +14 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +9 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +83 -24
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
- package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +89 -18
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/skc-runtime/state-writer.d.ts +5 -1
- package/dist/types/skc-runtime/team-launch.d.ts +1 -1
- package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
- package/dist/types/skc-runtime/team-store.d.ts +12 -0
- package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +133 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +22 -9
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +7 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -12
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/compile-args.ts +0 -1
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-docs-index.ts +4 -1
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +58 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +16 -100
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +6 -38
- package/src/cli/read-cli.ts +2 -1
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +133 -11
- package/src/commands/daemon.ts +22 -5
- package/src/commands/deep-interview.ts +26 -1
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +134 -17
- package/src/commands/team.ts +1 -1
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-contract.ts +174 -0
- package/src/config/model-profiles.ts +51 -10
- package/src/config/model-registry.ts +24 -4
- package/src/config/model-resolver.ts +2 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +157 -10
- package/src/config/settings.ts +254 -108
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
- package/src/defaults/skc/skills/team/SKILL.md +1 -1
- package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/eval/py/executor.ts +37 -13
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/extensibility/shared-events.ts +5 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/internal-urls/docs-index.generated.ts +17 -16
- package/src/internal-urls/local-protocol.ts +87 -25
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +120 -42
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +52 -0
- package/src/modes/acp/acp-agent.ts +647 -83
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/sayknow-pet-widget.ts +20 -8
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +26 -8
- package/src/modes/components/welcome.ts +50 -23
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +101 -54
- package/src/modes/controllers/event-controller.ts +119 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +100 -8
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +201 -60
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +6 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +99 -4
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +17 -3
- package/src/prompts/agents/critic.md +17 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -3
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
- package/src/runtime-mcp/json-rpc.ts +28 -11
- package/src/runtime-mcp/manager.ts +77 -59
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +293 -43
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +22 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +10 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +793 -121
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +87 -25
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/prompt-reconciliation.ts +240 -0
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
- package/src/sdk/bus/telegram-daemon-control.ts +161 -40
- package/src/sdk/bus/telegram-daemon.ts +3649 -593
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +8 -2
- package/src/sdk/host/control/operations.ts +3 -2
- package/src/sdk/host/host.ts +27 -9
- package/src/sdk/host/query/handlers.ts +125 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +145 -0
- package/src/sdk/protocol/operation-registry.ts +24 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +69 -8
- package/src/sdk/startup-capability.ts +15 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1112 -314
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +1426 -259
- package/src/session/internal/managed-session-storage.ts +414 -238
- package/src/session/internal/native-publish-outcome.ts +271 -0
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2079 -469
- package/src/session/session-storage.ts +151 -11
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +49 -5
- package/src/setup/provider-presets.json +2 -1
- package/src/skc-runtime/deep-interview-runtime.ts +303 -36
- package/src/skc-runtime/deep-interview-stage.ts +1016 -0
- package/src/skc-runtime/deep-interview-state.ts +8 -5
- package/src/skc-runtime/gc-render.ts +1 -0
- package/src/skc-runtime/gc-runtime.ts +11 -1
- package/src/skc-runtime/linux-proc.ts +70 -36
- package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/skc-runtime/ralplan-runtime.ts +1057 -75
- package/src/skc-runtime/repository-binding.ts +267 -0
- package/src/skc-runtime/session-state-sidecar.ts +25 -9
- package/src/skc-runtime/state-runtime.ts +50 -12
- package/src/skc-runtime/state-writer.ts +68 -56
- package/src/skc-runtime/team-launch.ts +34 -5
- package/src/skc-runtime/team-runtime.ts +41 -1
- package/src/skc-runtime/team-store.ts +32 -0
- package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/skc-runtime/team-workers.ts +1 -0
- package/src/skc-runtime/tmux-common.ts +13 -2
- package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
- package/src/skc-runtime/tmux-sessions.ts +45 -6
- package/src/skc-runtime/ultragoal-guard.ts +93 -15
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
- package/src/skc-runtime/ultragoal-runtime.ts +469 -38
- package/src/skc-runtime/workflow-manifest.generated.json +195 -5
- package/src/skc-runtime/workflow-manifest.ts +67 -3
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +104 -12
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/executor.ts +165 -47
- package/src/task/index.ts +123 -15
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/receipt.ts +3 -0
- package/src/task/render.ts +45 -26
- package/src/task/skc-command.ts +2 -2
- package/src/task/types.ts +55 -10
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +127 -154
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +7 -1
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1544 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
|
@@ -12,20 +12,107 @@ import {
|
|
|
12
12
|
openRecoveryFsRoot,
|
|
13
13
|
type RecoveryFsRoot,
|
|
14
14
|
renameNoReplacePath,
|
|
15
|
+
repairOwnerOnlyPathSecurityExpected,
|
|
15
16
|
snapshotDirectoryTree,
|
|
16
17
|
verifyOwnerOnlyFdSecurity,
|
|
17
18
|
verifyOwnerOnlyPathSecurity,
|
|
19
|
+
verifyOwnerOnlyPathSecurityExpected,
|
|
18
20
|
} from "@sayknow-cli/natives";
|
|
21
|
+
import {
|
|
22
|
+
classifyNativePublishOutcome,
|
|
23
|
+
formatNativePublishDiagnostic,
|
|
24
|
+
mayCleanCurrentStaging,
|
|
25
|
+
type NativePublishOutcome,
|
|
26
|
+
} from "./native-publish-outcome";
|
|
19
27
|
|
|
20
28
|
export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
|
|
21
|
-
export const MANAGED_ARTIFACT_MAX_FILES =
|
|
29
|
+
export const MANAGED_ARTIFACT_MAX_FILES = 50_000;
|
|
22
30
|
export const MANAGED_ARTIFACT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
23
31
|
export const MANAGED_ARTIFACT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
|
|
32
|
+
export const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
|
|
24
33
|
const LOCK_LEASE_MS = 60_000;
|
|
25
34
|
const LOCK_HEARTBEAT_MS = 10_000;
|
|
26
35
|
const LOCK_WAIT_MS = 5_000;
|
|
27
36
|
|
|
28
37
|
const LOCK_STALE_RECHECK_MS = 100;
|
|
38
|
+
export class ManagedPublishError extends Error {
|
|
39
|
+
readonly classification:
|
|
40
|
+
| "destination_conflict"
|
|
41
|
+
| "atomic_unavailable"
|
|
42
|
+
| "invalid_request"
|
|
43
|
+
| "durability_not_provable"
|
|
44
|
+
| "identity_mismatch"
|
|
45
|
+
| "io_error"
|
|
46
|
+
| "durability_failed";
|
|
47
|
+
readonly diagnostic: string;
|
|
48
|
+
|
|
49
|
+
constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome) {
|
|
50
|
+
super(classification);
|
|
51
|
+
this.name = "ManagedPublishError";
|
|
52
|
+
this.classification = classification;
|
|
53
|
+
this.diagnostic = formatNativePublishDiagnostic(outcome);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
|
|
58
|
+
const classification =
|
|
59
|
+
outcome.reason === "destination_exists"
|
|
60
|
+
? "destination_conflict"
|
|
61
|
+
: outcome.reason === "atomic_unavailable"
|
|
62
|
+
? "atomic_unavailable"
|
|
63
|
+
: outcome.reason === "invalid_request"
|
|
64
|
+
? "invalid_request"
|
|
65
|
+
: outcome.reason === "durability_not_provable"
|
|
66
|
+
? "durability_not_provable"
|
|
67
|
+
: outcome.reason === "identity_violation"
|
|
68
|
+
? "identity_mismatch"
|
|
69
|
+
: outcome.reason === "io_failure"
|
|
70
|
+
? "io_error"
|
|
71
|
+
: "durability_failed";
|
|
72
|
+
return new ManagedPublishError(classification, outcome);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
|
|
76
|
+
function sameDirectoryTreeSnapshotAfterRename(
|
|
77
|
+
left: NativeDirectoryTreeSnapshot,
|
|
78
|
+
right: NativeDirectoryTreeSnapshot,
|
|
79
|
+
): boolean {
|
|
80
|
+
return (
|
|
81
|
+
left.rootDev === right.rootDev &&
|
|
82
|
+
left.rootIno === right.rootIno &&
|
|
83
|
+
left.entries.length === right.entries.length &&
|
|
84
|
+
left.entries.every((entry, index) => {
|
|
85
|
+
const other = right.entries[index];
|
|
86
|
+
return (
|
|
87
|
+
other !== undefined &&
|
|
88
|
+
entry.relativePath === other.relativePath &&
|
|
89
|
+
entry.kind === other.kind &&
|
|
90
|
+
entry.dev === other.dev &&
|
|
91
|
+
entry.ino === other.ino &&
|
|
92
|
+
entry.size === other.size &&
|
|
93
|
+
entry.mtimeNs === other.mtimeNs &&
|
|
94
|
+
(entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
|
|
95
|
+
entry.sha256 === other.sha256
|
|
96
|
+
);
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class ManagedTreeMoveOutcomeError extends Error {
|
|
102
|
+
constructor(
|
|
103
|
+
message: string,
|
|
104
|
+
readonly stagingCleanupSafe: boolean,
|
|
105
|
+
) {
|
|
106
|
+
super(message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Cleanup is safe only when the native move reports that it did not commit. */
|
|
111
|
+
export function mayCleanManagedTreeStaging(error: unknown): boolean {
|
|
112
|
+
return !(error instanceof ManagedTreeMoveOutcomeError) || error.stagingCleanupSafe;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ManagedSessionSecurityPolicy = "default" | "windows-existing-verify-first";
|
|
29
116
|
|
|
30
117
|
export type ManagedStorageFailure =
|
|
31
118
|
| "migration_busy"
|
|
@@ -34,7 +121,10 @@ export type ManagedStorageFailure =
|
|
|
34
121
|
| "destination_conflict"
|
|
35
122
|
| "source_changed"
|
|
36
123
|
| "unsafe_artifacts"
|
|
124
|
+
| "artifact_capacity_exceeded"
|
|
37
125
|
| "durability_failed"
|
|
126
|
+
| "atomic_unavailable"
|
|
127
|
+
| "durability_not_provable"
|
|
38
128
|
| "migration_retired"
|
|
39
129
|
| "managed_storage_unsupported";
|
|
40
130
|
|
|
@@ -93,6 +183,9 @@ export function validateNativeSecurityResult(
|
|
|
93
183
|
) {
|
|
94
184
|
throw new Error("Malformed ACL security failure evidence");
|
|
95
185
|
}
|
|
186
|
+
if (operation === "verify" && result.operation !== "query") {
|
|
187
|
+
throw new Error("Verify failure unexpectedly reports ACL mutation");
|
|
188
|
+
}
|
|
96
189
|
} else if (result.operation !== undefined || result.attribute !== undefined) {
|
|
97
190
|
throw new Error("Unexpected ACL fields on owner-only security failure");
|
|
98
191
|
}
|
|
@@ -179,6 +272,40 @@ export interface ManagedDirectoryRoot {
|
|
|
179
272
|
readonly ino: bigint;
|
|
180
273
|
}
|
|
181
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Establish the first managed directory beneath a potentially shared ancestor.
|
|
277
|
+
* The ancestor is used only to create the managed chain; every component from
|
|
278
|
+
* `configuredRoot` downward is independently type, symlink, owner, mode, and
|
|
279
|
+
* ACL checked. This deliberately does not impose owner-only policy on `/tmp`.
|
|
280
|
+
*/
|
|
281
|
+
export function prepareManagedDirectoryRoot(
|
|
282
|
+
configuredRoot: string,
|
|
283
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
284
|
+
): ManagedDirectoryRoot {
|
|
285
|
+
const target = path.resolve(configuredRoot);
|
|
286
|
+
const missing: string[] = [];
|
|
287
|
+
let existing = target;
|
|
288
|
+
for (;;) {
|
|
289
|
+
try {
|
|
290
|
+
assertSafeDirectory(existing);
|
|
291
|
+
break;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
294
|
+
const parent = path.dirname(existing);
|
|
295
|
+
if (parent === existing) throw new Error(`Managed root is unavailable: ${configuredRoot}`);
|
|
296
|
+
missing.unshift(path.basename(existing));
|
|
297
|
+
existing = parent;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
let current = fs.realpathSync.native(existing);
|
|
301
|
+
for (const component of missing) {
|
|
302
|
+
current = path.join(current, component);
|
|
303
|
+
const created = ensureDirectoryComponent(current);
|
|
304
|
+
secureManagedDirectory(current, created, policy);
|
|
305
|
+
}
|
|
306
|
+
if (missing.length === 0) secureManagedDirectory(current, false, policy);
|
|
307
|
+
return managedDirectoryRoot(current);
|
|
308
|
+
}
|
|
182
309
|
export function managedDirectoryRoot(configuredRoot: string): ManagedDirectoryRoot {
|
|
183
310
|
const canonicalPath = fs.realpathSync.native(configuredRoot);
|
|
184
311
|
const stat = fs.lstatSync(canonicalPath, { bigint: true });
|
|
@@ -216,14 +343,16 @@ export function retainManagedDirectoryAuthority(
|
|
|
216
343
|
}
|
|
217
344
|
}
|
|
218
345
|
|
|
219
|
-
function ensureDirectoryComponent(pathname: string):
|
|
346
|
+
function ensureDirectoryComponent(pathname: string): boolean {
|
|
347
|
+
let created = false;
|
|
220
348
|
try {
|
|
221
349
|
fs.mkdirSync(pathname, { mode: 0o700 });
|
|
350
|
+
created = true;
|
|
222
351
|
} catch (error) {
|
|
223
352
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
224
353
|
}
|
|
225
354
|
assertSafeDirectory(pathname);
|
|
226
|
-
|
|
355
|
+
return created;
|
|
227
356
|
}
|
|
228
357
|
|
|
229
358
|
export function assertManagedDirectoryRoot(root: ManagedDirectoryRoot): void {
|
|
@@ -263,10 +392,66 @@ function secure(pathname: string, kind: "directory" | "file"): void {
|
|
|
263
392
|
if (!verified.ok) throw securityError(pathname, verified);
|
|
264
393
|
}
|
|
265
394
|
|
|
395
|
+
function windowsExistingVerifyFirst(policy: ManagedSessionSecurityPolicy): boolean {
|
|
396
|
+
return process.platform === "win32" && policy === "windows-existing-verify-first";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function assertManagedPathIdentity(pathname: string, kind: "directory" | "file", expected: fs.BigIntStats): void {
|
|
400
|
+
const current = fs.lstatSync(pathname, { bigint: true });
|
|
401
|
+
if (current.isSymbolicLink()) throw new Error("reparse_point");
|
|
402
|
+
const expectedKind = kind === "directory" ? current.isDirectory() : current.isFile();
|
|
403
|
+
if (!expectedKind) throw new Error(kind === "directory" ? "not_directory" : "not_file");
|
|
404
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino) throw new Error("identity_mismatch");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function verifyExistingManagedPathSecurity(
|
|
408
|
+
pathname: string,
|
|
409
|
+
kind: "directory" | "file",
|
|
410
|
+
expected: fs.BigIntStats,
|
|
411
|
+
): void {
|
|
412
|
+
const verified = validateNativeSecurityResult(
|
|
413
|
+
verifyOwnerOnlyPathSecurityExpected(pathname, kind, expected.dev, expected.ino),
|
|
414
|
+
"verify",
|
|
415
|
+
kind,
|
|
416
|
+
);
|
|
417
|
+
assertManagedPathIdentity(pathname, kind, expected);
|
|
418
|
+
if (!verified.ok) throw securityError(pathname, verified);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function secureExistingManagedDirectory(pathname: string, kind: "directory" | "file"): void {
|
|
422
|
+
const named = fs.lstatSync(pathname, { bigint: true });
|
|
423
|
+
const safeKind = kind === "directory" ? named.isDirectory() : named.isFile();
|
|
424
|
+
if (!safeKind || named.isSymbolicLink()) throw new Error(`Unsafe managed ${kind}: ${pathname}`);
|
|
425
|
+
const verified = validateNativeSecurityResult(
|
|
426
|
+
verifyOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
|
|
427
|
+
"verify",
|
|
428
|
+
kind,
|
|
429
|
+
);
|
|
430
|
+
assertManagedPathIdentity(pathname, kind, named);
|
|
431
|
+
if (verified.ok) return;
|
|
432
|
+
if (verified.code !== "acl_verify_failed") throw securityError(pathname, verified);
|
|
433
|
+
const repaired = validateNativeSecurityResult(
|
|
434
|
+
repairOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
|
|
435
|
+
"verify",
|
|
436
|
+
kind,
|
|
437
|
+
);
|
|
438
|
+
if (!repaired.ok) throw securityError(pathname, repaired);
|
|
439
|
+
assertManagedPathIdentity(pathname, kind, named);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function secureManagedDirectory(pathname: string, created: boolean, policy: ManagedSessionSecurityPolicy): void {
|
|
443
|
+
if (!created && windowsExistingVerifyFirst(policy)) {
|
|
444
|
+
secureExistingManagedDirectory(pathname, "directory");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
secure(pathname, "directory");
|
|
448
|
+
}
|
|
449
|
+
|
|
266
450
|
/** Internal retained-root capability for one managed descendant subtree. */
|
|
267
451
|
export class ManagedSessionDescendantStore {
|
|
268
452
|
readonly #root: ManagedDirectoryRoot;
|
|
269
453
|
readonly #baseDir: string;
|
|
454
|
+
readonly #policy: ManagedSessionSecurityPolicy;
|
|
270
455
|
readonly #authority: RecoveryFsRoot | undefined;
|
|
271
456
|
readonly #authorityBaseDir: string;
|
|
272
457
|
readonly #subtreeRoot: ManagedDirectoryRoot;
|
|
@@ -275,32 +460,31 @@ export class ManagedSessionDescendantStore {
|
|
|
275
460
|
root: ManagedDirectoryRoot,
|
|
276
461
|
baseDir: string,
|
|
277
462
|
retained?: { authority: RecoveryFsRoot; authorityBaseDir: string },
|
|
463
|
+
policy?: ManagedSessionSecurityPolicy,
|
|
278
464
|
) {
|
|
279
|
-
assertManagedDirectoryRoot(root);
|
|
280
465
|
managedRelativePath(root, baseDir);
|
|
466
|
+
|
|
281
467
|
this.#root = root;
|
|
282
468
|
this.#baseDir = path.resolve(baseDir);
|
|
469
|
+
this.#policy = policy ?? "default";
|
|
283
470
|
this.#authorityBaseDir = retained?.authorityBaseDir ?? this.#baseDir;
|
|
284
471
|
if (retained) {
|
|
285
472
|
const relative = path.relative(retained.authorityBaseDir, this.#baseDir).split(path.sep).join("/");
|
|
286
473
|
const captured = retained.authority.snapshotManagedTree(relative);
|
|
287
474
|
if (!captured.ok || !captured.snapshot)
|
|
288
475
|
throw new Error(captured.code ?? "Managed subtree identity unavailable");
|
|
289
|
-
const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
290
|
-
if (
|
|
291
|
-
captured.snapshot.rootDev !== subtreeStat.dev.toString() ||
|
|
292
|
-
captured.snapshot.rootIno !== subtreeStat.ino.toString()
|
|
293
|
-
)
|
|
294
|
-
throw new Error("Managed descendant root identity changed");
|
|
295
476
|
this.#subtreeRoot = Object.freeze({
|
|
296
477
|
canonicalPath: this.#baseDir,
|
|
297
|
-
dev:
|
|
298
|
-
ino:
|
|
478
|
+
dev: BigInt(captured.snapshot.rootDev),
|
|
479
|
+
ino: BigInt(captured.snapshot.rootIno),
|
|
299
480
|
});
|
|
300
481
|
this.#authority = retained.authority;
|
|
482
|
+
this.#assertBound();
|
|
483
|
+
|
|
301
484
|
return;
|
|
302
485
|
}
|
|
303
|
-
|
|
486
|
+
assertManagedDirectoryRoot(root);
|
|
487
|
+
ensureManagedDirectory(this.#baseDir, root, this.#policy);
|
|
304
488
|
const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
305
489
|
this.#subtreeRoot = Object.freeze({ canonicalPath: this.#baseDir, dev: subtreeStat.dev, ino: subtreeStat.ino });
|
|
306
490
|
if (process.platform === "linux") {
|
|
@@ -332,19 +516,28 @@ export class ManagedSessionDescendantStore {
|
|
|
332
516
|
return this.#subtreeRoot;
|
|
333
517
|
}
|
|
334
518
|
|
|
519
|
+
get securityPolicy(): ManagedSessionSecurityPolicy {
|
|
520
|
+
return this.#policy;
|
|
521
|
+
}
|
|
522
|
+
|
|
335
523
|
deriveSubtree(relativePath: string): ManagedSessionDescendantStore {
|
|
336
524
|
const child = this.ensureDirectory(relativePath);
|
|
337
525
|
const resolved = this.#resolve(relativePath);
|
|
338
|
-
if (!this.#authority) return new ManagedSessionDescendantStore(this.#root, resolved);
|
|
526
|
+
if (!this.#authority) return new ManagedSessionDescendantStore(this.#root, resolved, undefined, this.#policy);
|
|
339
527
|
const retainedChild = this.#authority.retainManagedDirectory(
|
|
340
528
|
this.#relative(resolved),
|
|
341
529
|
child.dev.toString(),
|
|
342
530
|
child.ino.toString(),
|
|
343
531
|
);
|
|
344
|
-
return new ManagedSessionDescendantStore(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
532
|
+
return new ManagedSessionDescendantStore(
|
|
533
|
+
this.#root,
|
|
534
|
+
resolved,
|
|
535
|
+
{
|
|
536
|
+
authority: retainedChild,
|
|
537
|
+
authorityBaseDir: resolved,
|
|
538
|
+
},
|
|
539
|
+
this.#policy,
|
|
540
|
+
);
|
|
348
541
|
}
|
|
349
542
|
|
|
350
543
|
retainAuthority(): RecoveryFsRoot | undefined {
|
|
@@ -371,12 +564,19 @@ export class ManagedSessionDescendantStore {
|
|
|
371
564
|
return;
|
|
372
565
|
}
|
|
373
566
|
} else {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
567
|
+
const named = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
568
|
+
if (!named.isDirectory() || named.isSymbolicLink())
|
|
569
|
+
throw new Error(`Unsafe managed directory: ${this.#baseDir}`);
|
|
570
|
+
if (windowsExistingVerifyFirst(this.#policy))
|
|
571
|
+
verifyExistingManagedPathSecurity(this.#baseDir, "directory", named);
|
|
572
|
+
else {
|
|
573
|
+
const verified = validateNativeSecurityResult(
|
|
574
|
+
verifyOwnerOnlyPathSecurity(this.#baseDir, "directory"),
|
|
575
|
+
"verify",
|
|
576
|
+
"directory",
|
|
577
|
+
);
|
|
578
|
+
if (!verified.ok) throw securityError(this.#baseDir, verified);
|
|
579
|
+
}
|
|
380
580
|
}
|
|
381
581
|
this.#assertBound();
|
|
382
582
|
}
|
|
@@ -421,7 +621,7 @@ export class ManagedSessionDescendantStore {
|
|
|
421
621
|
ino: BigInt(ensured.identity.ino),
|
|
422
622
|
});
|
|
423
623
|
}
|
|
424
|
-
ensureManagedDirectory(this.#resolve(relativePath), this.#root);
|
|
624
|
+
ensureManagedDirectory(this.#resolve(relativePath), this.#root, this.#policy);
|
|
425
625
|
const named = fs.lstatSync(this.#resolve(relativePath), { bigint: true });
|
|
426
626
|
return Object.freeze({ canonicalPath: this.#resolve(relativePath), dev: named.dev, ino: named.ino });
|
|
427
627
|
}
|
|
@@ -434,13 +634,13 @@ export class ManagedSessionDescendantStore {
|
|
|
434
634
|
this.#assertBound();
|
|
435
635
|
return;
|
|
436
636
|
}
|
|
437
|
-
await publishManagedFileNoReplace(resolved, bytes, undefined, this.#root);
|
|
637
|
+
await publishManagedFileNoReplace(resolved, bytes, undefined, this.#root, this.#policy);
|
|
438
638
|
}
|
|
439
639
|
|
|
440
640
|
publishNoReplaceSync(relativePath: string, bytes: Uint8Array): void {
|
|
441
641
|
const resolved = this.#resolve(relativePath);
|
|
442
642
|
if (!this.#authority) {
|
|
443
|
-
publishManagedFileNoReplaceSync(resolved, bytes, this.#root);
|
|
643
|
+
publishManagedFileNoReplaceSync(resolved, bytes, this.#root, this.#policy);
|
|
444
644
|
this.#assertBound();
|
|
445
645
|
return;
|
|
446
646
|
}
|
|
@@ -453,7 +653,7 @@ export class ManagedSessionDescendantStore {
|
|
|
453
653
|
this.#assertBound();
|
|
454
654
|
const resolved = this.#resolve(relativePath);
|
|
455
655
|
if (!this.#authority) {
|
|
456
|
-
await replaceManagedFile(resolved, bytes, this.#subtreeRoot);
|
|
656
|
+
await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
457
657
|
this.#assertBound();
|
|
458
658
|
return;
|
|
459
659
|
}
|
|
@@ -466,7 +666,7 @@ export class ManagedSessionDescendantStore {
|
|
|
466
666
|
this.#assertBound();
|
|
467
667
|
const resolved = this.#resolve(relativePath);
|
|
468
668
|
if (!this.#authority) {
|
|
469
|
-
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot);
|
|
669
|
+
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
470
670
|
this.#assertBound();
|
|
471
671
|
return;
|
|
472
672
|
}
|
|
@@ -477,7 +677,7 @@ export class ManagedSessionDescendantStore {
|
|
|
477
677
|
appendSync(relativePath: string, bytes: Uint8Array): void {
|
|
478
678
|
this.#assertBound();
|
|
479
679
|
const resolved = this.#resolve(relativePath);
|
|
480
|
-
|
|
680
|
+
let existing = this.readExpected(relativePath);
|
|
481
681
|
if (!existing) throw new Error("managed_append_missing");
|
|
482
682
|
if (this.#authority) {
|
|
483
683
|
const appended = this.#authority.appendManaged(
|
|
@@ -494,20 +694,49 @@ export class ManagedSessionDescendantStore {
|
|
|
494
694
|
this.#assertBound();
|
|
495
695
|
return;
|
|
496
696
|
}
|
|
697
|
+
// On Darwin, the first write-append open can change only ctime (e.g. com.apple.provenance)
|
|
698
|
+
// while leaving dev/ino/size/mtime/content intact. Allow one bounded refresh+retry for that
|
|
699
|
+
// transition only; any other identity change or a second ctime transition stays fail-closed.
|
|
700
|
+
// See https://github.com/jaybeyond/Sayknow_CLI/issues/2944
|
|
497
701
|
let fd: number | undefined;
|
|
702
|
+
let darwinCtimeRefreshConsumed = false;
|
|
498
703
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
704
|
+
for (;;) {
|
|
705
|
+
fd = fs.openSync(resolved, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_NOFOLLOW);
|
|
706
|
+
const before = identity(fs.fstatSync(fd, { bigint: true }));
|
|
707
|
+
if (!sameIdentity(before, existing.identity)) {
|
|
708
|
+
const allowDarwinCtimeRefresh =
|
|
709
|
+
process.platform === "darwin" &&
|
|
710
|
+
!darwinCtimeRefreshConsumed &&
|
|
711
|
+
isCtimeOnlyIdentityMismatch(before, existing.identity);
|
|
712
|
+
if (!allowDarwinCtimeRefresh) throw new Error("identity_mismatch");
|
|
713
|
+
fs.closeSync(fd);
|
|
714
|
+
fd = undefined;
|
|
715
|
+
darwinCtimeRefreshConsumed = true;
|
|
716
|
+
const original = existing;
|
|
717
|
+
const refreshed = this.readExpected(relativePath);
|
|
718
|
+
if (
|
|
719
|
+
!refreshed ||
|
|
720
|
+
!sameStableIdentityIgnoringCtime(refreshed.identity, original.identity) ||
|
|
721
|
+
refreshed.identity.sha256 !== original.identity.sha256 ||
|
|
722
|
+
!refreshed.bytes.equals(original.bytes)
|
|
723
|
+
) {
|
|
724
|
+
throw new Error("identity_mismatch");
|
|
725
|
+
}
|
|
726
|
+
existing = refreshed;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
secureFileDescriptor(resolved, fd, "verify");
|
|
730
|
+
let offset = 0;
|
|
731
|
+
while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
732
|
+
fs.fsyncSync(fd);
|
|
733
|
+
secureFileDescriptor(resolved, fd, "verify");
|
|
734
|
+
const after = identity(fs.fstatSync(fd, { bigint: true }));
|
|
735
|
+
const named = identity(fs.lstatSync(resolved, { bigint: true }));
|
|
736
|
+
if (!sameIdentity(after, named) || after.dev !== before.dev || after.ino !== before.ino)
|
|
737
|
+
throw new Error("identity_mismatch");
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
511
740
|
} finally {
|
|
512
741
|
if (fd !== undefined) fs.closeSync(fd);
|
|
513
742
|
}
|
|
@@ -528,6 +757,7 @@ export class ManagedSessionDescendantStore {
|
|
|
528
757
|
if (!created.ok) throw new Error(created.code ?? "managed_publish_failed");
|
|
529
758
|
if (!created.identity) throw new Error("managed_publish_identity_unavailable");
|
|
530
759
|
let captured: ReturnType<RecoveryFsRoot["readManaged"]> | undefined;
|
|
760
|
+
let published: unknown;
|
|
531
761
|
try {
|
|
532
762
|
captured = this.#authority.readManaged(temporary);
|
|
533
763
|
if (!captured.ok || !captured.identity || !captured.data)
|
|
@@ -553,7 +783,7 @@ export class ManagedSessionDescendantStore {
|
|
|
553
783
|
digest,
|
|
554
784
|
);
|
|
555
785
|
if (!synced.ok) throw new Error(synced.code ?? "managed_publish_fsync_failed");
|
|
556
|
-
|
|
786
|
+
published = this.#authority.renameManagedFileNoReplace(
|
|
557
787
|
temporary,
|
|
558
788
|
relative,
|
|
559
789
|
captured.identity.dev,
|
|
@@ -563,35 +793,14 @@ export class ManagedSessionDescendantStore {
|
|
|
563
793
|
captured.identity.ctimeNs,
|
|
564
794
|
digest,
|
|
565
795
|
);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
published.code === "already_exists"
|
|
569
|
-
? "destination_conflict"
|
|
570
|
-
: (published.code ?? "managed_publish_failed"),
|
|
571
|
-
);
|
|
796
|
+
const outcome = classifyNativePublishOutcome(published, "retained_file");
|
|
797
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
572
798
|
} catch (error) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
installed.identity &&
|
|
579
|
-
installed.data &&
|
|
580
|
-
installed.identity.dev === captured.identity.dev &&
|
|
581
|
-
installed.identity.ino === captured.identity.ino &&
|
|
582
|
-
(installed.identity.sha256 ?? createHash("sha256").update(installed.data).digest("hex")) === digest
|
|
583
|
-
) {
|
|
584
|
-
const removed = this.#authority.removeManaged(
|
|
585
|
-
relative,
|
|
586
|
-
installed.identity.dev,
|
|
587
|
-
installed.identity.ino,
|
|
588
|
-
installed.identity.size,
|
|
589
|
-
installed.identity.mtimeNs,
|
|
590
|
-
installed.identity.ctimeNs,
|
|
591
|
-
digest,
|
|
592
|
-
);
|
|
593
|
-
if (!removed.ok) throw new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
594
|
-
}
|
|
799
|
+
// A committed or unknown native outcome is evidence, not authorization to
|
|
800
|
+
// probe or remove the destination. Only a validated pre-mutation result
|
|
801
|
+
// may clean this operation's still-named staging object.
|
|
802
|
+
const outcome = captured ? classifyNativePublishOutcome(published, "retained_file") : undefined;
|
|
803
|
+
if (captured?.ok && captured.identity && captured.data && outcome && mayCleanCurrentStaging(outcome)) {
|
|
595
804
|
const staged = this.#authority.readManaged(temporary);
|
|
596
805
|
if (
|
|
597
806
|
staged.ok &&
|
|
@@ -609,7 +818,8 @@ export class ManagedSessionDescendantStore {
|
|
|
609
818
|
staged.identity.ctimeNs,
|
|
610
819
|
staged.identity.sha256 ?? createHash("sha256").update(staged.data).digest("hex"),
|
|
611
820
|
);
|
|
612
|
-
if (!removed.ok
|
|
821
|
+
if (!removed.ok && removed.code !== "cleanup_pending")
|
|
822
|
+
throw new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
613
823
|
}
|
|
614
824
|
}
|
|
615
825
|
throw error;
|
|
@@ -682,7 +892,17 @@ export class ManagedSessionDescendantStore {
|
|
|
682
892
|
sha256: expected.identity.sha256,
|
|
683
893
|
quarantineName: `.skc-remove-${process.pid}-${randomUUID()}`,
|
|
684
894
|
});
|
|
685
|
-
if (
|
|
895
|
+
if (
|
|
896
|
+
!removed.ok &&
|
|
897
|
+
!(
|
|
898
|
+
removed.code === "cleanup_pending" &&
|
|
899
|
+
(removed.detachedPath ??
|
|
900
|
+
removed.retainedSuccessorPath ??
|
|
901
|
+
removed.retainedPlaceholderPath ??
|
|
902
|
+
removed.retainedUnknownPath) !== undefined
|
|
903
|
+
)
|
|
904
|
+
)
|
|
905
|
+
throw new Error(removed.code ?? "managed_remove_failed");
|
|
686
906
|
this.#assertBound();
|
|
687
907
|
return;
|
|
688
908
|
}
|
|
@@ -695,7 +915,8 @@ export class ManagedSessionDescendantStore {
|
|
|
695
915
|
expected.identity.ctimeNs.toString(),
|
|
696
916
|
expected.identity.sha256,
|
|
697
917
|
);
|
|
698
|
-
if (!removed.ok
|
|
918
|
+
if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
|
|
919
|
+
throw new Error(removed.code ?? "managed_remove_failed");
|
|
699
920
|
this.#assertBound();
|
|
700
921
|
}
|
|
701
922
|
/** Read and remove one managed descendant through retained authority. */
|
|
@@ -719,7 +940,8 @@ export class ManagedSessionDescendantStore {
|
|
|
719
940
|
existing.identity.ctimeNs,
|
|
720
941
|
existing.identity.sha256 ?? createHash("sha256").update(existing.data).digest("hex"),
|
|
721
942
|
);
|
|
722
|
-
if (!removed.ok
|
|
943
|
+
if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
|
|
944
|
+
throw new Error(removed.code ?? "managed_consume_failed");
|
|
723
945
|
this.#assertBound();
|
|
724
946
|
return existing.data;
|
|
725
947
|
}
|
|
@@ -805,92 +1027,31 @@ export class ManagedSessionDescendantStore {
|
|
|
805
1027
|
sourceRelativePath: string,
|
|
806
1028
|
destinationRelativePath: string,
|
|
807
1029
|
expected: NativeDirectoryTreeSnapshot,
|
|
808
|
-
):
|
|
1030
|
+
): NativeDirectoryTreeSnapshot {
|
|
809
1031
|
this.#assertBound();
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
try {
|
|
816
|
-
this.ensureDirectory(destinationRelativePath);
|
|
817
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
818
|
-
for (const entry of expected.entries) {
|
|
819
|
-
if (entry.relativePath === "") continue;
|
|
820
|
-
const target = path.posix.join(destinationRelativePath.replaceAll(path.sep, "/"), entry.relativePath);
|
|
821
|
-
if (entry.kind === "directory") {
|
|
822
|
-
this.ensureDirectory(target);
|
|
823
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
const sourceEntry = this.readExpected(
|
|
827
|
-
path.posix.join(sourceRelativePath.replaceAll(path.sep, "/"), entry.relativePath),
|
|
828
|
-
);
|
|
829
|
-
if (
|
|
830
|
-
!sourceEntry ||
|
|
831
|
-
sourceEntry.identity.dev.toString() !== entry.dev ||
|
|
832
|
-
sourceEntry.identity.ino.toString() !== entry.ino ||
|
|
833
|
-
sourceEntry.identity.size.toString() !== entry.size ||
|
|
834
|
-
sourceEntry.identity.mtimeNs.toString() !== entry.mtimeNs ||
|
|
835
|
-
sourceEntry.identity.sha256 !== entry.sha256
|
|
836
|
-
)
|
|
837
|
-
throw new Error("artifact_source_changed");
|
|
838
|
-
this.publishNoReplaceSync(target, sourceEntry.bytes);
|
|
839
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
840
|
-
}
|
|
841
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
842
|
-
const comparable = (tree: NativeDirectoryTreeSnapshot) =>
|
|
843
|
-
tree.entries.map(entry => ({
|
|
844
|
-
relativePath: entry.relativePath,
|
|
845
|
-
kind: entry.kind,
|
|
846
|
-
size: entry.size,
|
|
847
|
-
sha256: entry.sha256,
|
|
848
|
-
}));
|
|
849
|
-
if (JSON.stringify(comparable(destinationSnapshot)) !== JSON.stringify(comparable(expected)))
|
|
850
|
-
throw new Error("artifact_destination_mismatch");
|
|
851
|
-
const terminalSource = snapshotDirectoryTree(source);
|
|
852
|
-
if (
|
|
853
|
-
!terminalSource.ok ||
|
|
854
|
-
!terminalSource.snapshot ||
|
|
855
|
-
JSON.stringify(terminalSource.snapshot) !== JSON.stringify(expected)
|
|
1032
|
+
const moved = this.#authority
|
|
1033
|
+
? this.#authority.renameManagedTreeNoReplace(
|
|
1034
|
+
this.#relative(this.#resolve(sourceRelativePath)),
|
|
1035
|
+
this.#relative(this.#resolve(destinationRelativePath)),
|
|
1036
|
+
expected,
|
|
856
1037
|
)
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
this.#relative(this.#resolve(destinationRelativePath)),
|
|
873
|
-
expected,
|
|
874
|
-
);
|
|
875
|
-
if (!moved.ok) {
|
|
876
|
-
try {
|
|
877
|
-
const installed = this.captureTree(destinationRelativePath);
|
|
878
|
-
const comparable = (tree: NativeDirectoryTreeSnapshot) =>
|
|
879
|
-
tree.entries.map(entry => ({
|
|
880
|
-
relativePath: entry.relativePath,
|
|
881
|
-
kind: entry.kind,
|
|
882
|
-
size: entry.size,
|
|
883
|
-
sha256: entry.sha256,
|
|
884
|
-
}));
|
|
885
|
-
if (JSON.stringify(comparable(installed)) !== JSON.stringify(comparable(expected)))
|
|
886
|
-
throw new Error("managed_publish_reconcile_failed");
|
|
887
|
-
this.removeTreeExpected(destinationRelativePath, installed);
|
|
888
|
-
} catch (error) {
|
|
889
|
-
if (!(error instanceof Error) || error.message !== "not_found") throw error;
|
|
890
|
-
}
|
|
891
|
-
throw new Error(moved.code ?? "managed_move_failed");
|
|
892
|
-
}
|
|
1038
|
+
: renameNoReplacePath(this.#resolve(sourceRelativePath), this.#resolve(destinationRelativePath));
|
|
1039
|
+
const outcome = classifyNativePublishOutcome(moved, this.#authority ? "retained_tree" : "direct_rename");
|
|
1040
|
+
if (!outcome.ok)
|
|
1041
|
+
throw new ManagedTreeMoveOutcomeError(publishFailure(outcome).message, mayCleanCurrentStaging(outcome));
|
|
1042
|
+
const movedSnapshot = this.#authority
|
|
1043
|
+
? this.#authority.snapshotManagedTree(this.#relative(this.#resolve(destinationRelativePath)))
|
|
1044
|
+
: snapshotDirectoryTree(this.#resolve(destinationRelativePath));
|
|
1045
|
+
if (
|
|
1046
|
+
!movedSnapshot.ok ||
|
|
1047
|
+
!movedSnapshot.snapshot ||
|
|
1048
|
+
!sameDirectoryTreeSnapshotAfterRename(movedSnapshot.snapshot, expected)
|
|
1049
|
+
)
|
|
1050
|
+
throw new ManagedTreeMoveOutcomeError("artifact_destination_mismatch", false);
|
|
1051
|
+
if (!this.#authority) fsyncDirectory(this.#baseDir);
|
|
1052
|
+
|
|
893
1053
|
this.#assertBound();
|
|
1054
|
+
return movedSnapshot.snapshot;
|
|
894
1055
|
}
|
|
895
1056
|
|
|
896
1057
|
removeTreeExpected(relativePath: string, expected: NativeDirectoryTreeSnapshot): void {
|
|
@@ -1018,6 +1179,24 @@ function sameIdentity(left: ManagedFileSnapshot["identity"], right: ManagedFileS
|
|
|
1018
1179
|
);
|
|
1019
1180
|
}
|
|
1020
1181
|
|
|
1182
|
+
/** Content/path identity that remains meaningful across a Darwin write-open ctime-only transition. */
|
|
1183
|
+
function sameStableIdentityIgnoringCtime(
|
|
1184
|
+
left: ManagedFileSnapshot["identity"],
|
|
1185
|
+
right: ManagedFileSnapshot["identity"],
|
|
1186
|
+
): boolean {
|
|
1187
|
+
return (
|
|
1188
|
+
left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** True when observed identity matches expected on every field except ctimeNs. */
|
|
1193
|
+
function isCtimeOnlyIdentityMismatch(
|
|
1194
|
+
observed: ManagedFileSnapshot["identity"],
|
|
1195
|
+
expected: ManagedFileSnapshot["identity"],
|
|
1196
|
+
): boolean {
|
|
1197
|
+
return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1021
1200
|
function parseLock(pathname: string): LockRecord | undefined {
|
|
1022
1201
|
try {
|
|
1023
1202
|
const stat = fs.lstatSync(pathname);
|
|
@@ -1060,25 +1239,43 @@ function sameFileIdentity(left: fs.BigIntStats, right: fs.BigIntStats): boolean
|
|
|
1060
1239
|
}
|
|
1061
1240
|
|
|
1062
1241
|
/** Create a managed directory and fail closed unless its owner-only mode/ACL verifies. */
|
|
1063
|
-
export function ensureManagedDirectory(
|
|
1242
|
+
export function ensureManagedDirectory(
|
|
1243
|
+
pathname: string,
|
|
1244
|
+
root?: ManagedDirectoryRoot,
|
|
1245
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1246
|
+
): void {
|
|
1064
1247
|
if (!root) {
|
|
1248
|
+
let existed = false;
|
|
1249
|
+
try {
|
|
1250
|
+
const named = fs.lstatSync(pathname);
|
|
1251
|
+
existed = true;
|
|
1252
|
+
if (!named.isDirectory() || named.isSymbolicLink()) throw new Error(`Unsafe managed directory: ${pathname}`);
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1255
|
+
}
|
|
1065
1256
|
fs.mkdirSync(pathname, { recursive: true, mode: 0o700 });
|
|
1066
1257
|
assertSafeDirectory(pathname);
|
|
1067
|
-
|
|
1258
|
+
if (existed && windowsExistingVerifyFirst(policy)) secureExistingManagedDirectory(pathname, "directory");
|
|
1259
|
+
else secure(pathname, "directory");
|
|
1068
1260
|
return;
|
|
1069
1261
|
}
|
|
1070
1262
|
ensureManagedRoot(root);
|
|
1263
|
+
const components = managedRelativePath(root, pathname);
|
|
1264
|
+
if (components.length === 0) {
|
|
1265
|
+
if (windowsExistingVerifyFirst(policy)) secureExistingManagedDirectory(root.canonicalPath, "directory");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1071
1268
|
let current = root.canonicalPath;
|
|
1072
|
-
|
|
1269
|
+
if (windowsExistingVerifyFirst(policy)) secureExistingManagedDirectory(current, "directory");
|
|
1270
|
+
else secure(current, "directory");
|
|
1271
|
+
for (const component of components) {
|
|
1073
1272
|
// Re-inspect the captured root or already-secured descendant before every
|
|
1074
1273
|
// descent; a replaced component cannot grant authority to the next one.
|
|
1075
1274
|
if (current === root.canonicalPath) assertManagedDirectoryRoot(root);
|
|
1076
|
-
else
|
|
1077
|
-
assertSafeDirectory(current);
|
|
1078
|
-
secure(current, "directory");
|
|
1079
|
-
}
|
|
1275
|
+
else assertSafeDirectory(current);
|
|
1080
1276
|
current = path.join(current, component);
|
|
1081
|
-
ensureDirectoryComponent(current);
|
|
1277
|
+
const created = ensureDirectoryComponent(current);
|
|
1278
|
+
secureManagedDirectory(current, created, policy);
|
|
1082
1279
|
}
|
|
1083
1280
|
}
|
|
1084
1281
|
|
|
@@ -1123,15 +1320,17 @@ export async function publishManagedFileNoReplace(
|
|
|
1123
1320
|
bytes: Uint8Array,
|
|
1124
1321
|
assertOwned?: () => void,
|
|
1125
1322
|
root?: ManagedDirectoryRoot,
|
|
1323
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1126
1324
|
): Promise<void> {
|
|
1127
1325
|
const parent = path.dirname(destination);
|
|
1128
|
-
ensureManagedDirectory(parent, root);
|
|
1326
|
+
ensureManagedDirectory(parent, root, policy);
|
|
1129
1327
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1130
1328
|
let fd: number | undefined;
|
|
1131
1329
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1132
|
-
let stagingSnapshot: ManagedFileSnapshot | undefined;
|
|
1133
|
-
let committed = false;
|
|
1134
1330
|
let failure: unknown;
|
|
1331
|
+
let outcome: NativePublishOutcome | undefined;
|
|
1332
|
+
let renameAttempted = false;
|
|
1333
|
+
|
|
1135
1334
|
try {
|
|
1136
1335
|
assertOwned?.();
|
|
1137
1336
|
fd = fs.openSync(
|
|
@@ -1146,17 +1345,13 @@ export async function publishManagedFileNoReplace(
|
|
|
1146
1345
|
secureFileDescriptor(staging, fd, "verify");
|
|
1147
1346
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1148
1347
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1149
|
-
stagingSnapshot = {
|
|
1150
|
-
bytes: Buffer.from(bytes),
|
|
1151
|
-
identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
|
|
1152
|
-
};
|
|
1153
1348
|
assertOwned?.();
|
|
1154
1349
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1350
|
+
renameAttempted = true;
|
|
1351
|
+
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1352
|
+
|
|
1353
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
1354
|
+
|
|
1160
1355
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1161
1356
|
if (
|
|
1162
1357
|
!named.isFile() ||
|
|
@@ -1171,25 +1366,12 @@ export async function publishManagedFileNoReplace(
|
|
|
1171
1366
|
fd = undefined;
|
|
1172
1367
|
|
|
1173
1368
|
fsyncDirectory(parent);
|
|
1174
|
-
committed = true;
|
|
1175
1369
|
} catch (error) {
|
|
1176
1370
|
failure = error;
|
|
1177
1371
|
} finally {
|
|
1178
1372
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
const installed = captureManagedFileNoFollow(destination);
|
|
1182
|
-
if (
|
|
1183
|
-
installed.identity.dev === stagingSnapshot.identity.dev &&
|
|
1184
|
-
installed.identity.ino === stagingSnapshot.identity.ino &&
|
|
1185
|
-
installed.identity.sha256 === stagingSnapshot.identity.sha256
|
|
1186
|
-
)
|
|
1187
|
-
await unlinkManagedFileVerified(destination, installed.identity);
|
|
1188
|
-
} catch (error) {
|
|
1189
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
if (stagingIdentity) {
|
|
1373
|
+
|
|
1374
|
+
if (stagingIdentity && (!renameAttempted || (outcome && mayCleanCurrentStaging(outcome)))) {
|
|
1193
1375
|
await fsp
|
|
1194
1376
|
.lstat(staging, { bigint: true })
|
|
1195
1377
|
.then(stat => {
|
|
@@ -1208,15 +1390,16 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1208
1390
|
destination: string,
|
|
1209
1391
|
bytes: Uint8Array,
|
|
1210
1392
|
root?: ManagedDirectoryRoot,
|
|
1393
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1211
1394
|
): void {
|
|
1212
1395
|
const parent = path.dirname(destination);
|
|
1213
|
-
ensureManagedDirectory(parent, root);
|
|
1396
|
+
ensureManagedDirectory(parent, root, policy);
|
|
1214
1397
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1215
1398
|
let fd: number | undefined;
|
|
1216
1399
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1217
|
-
let stagingSnapshot: ManagedFileSnapshot | undefined;
|
|
1218
|
-
let committed = false;
|
|
1219
1400
|
let failure: unknown;
|
|
1401
|
+
let outcome: NativePublishOutcome | undefined;
|
|
1402
|
+
|
|
1220
1403
|
try {
|
|
1221
1404
|
fd = fs.openSync(
|
|
1222
1405
|
staging,
|
|
@@ -1230,16 +1413,10 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1230
1413
|
secureFileDescriptor(staging, fd, "verify");
|
|
1231
1414
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1232
1415
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1233
|
-
stagingSnapshot = {
|
|
1234
|
-
bytes: Buffer.from(bytes),
|
|
1235
|
-
identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
|
|
1236
|
-
};
|
|
1237
1416
|
|
|
1238
|
-
|
|
1239
|
-
if (!
|
|
1240
|
-
|
|
1241
|
-
throw new Error("durability_failed");
|
|
1242
|
-
}
|
|
1417
|
+
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1418
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
1419
|
+
|
|
1243
1420
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1244
1421
|
if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
|
|
1245
1422
|
throw new Error("destination_identity_changed");
|
|
@@ -1248,35 +1425,12 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1248
1425
|
fs.closeSync(fd);
|
|
1249
1426
|
fd = undefined;
|
|
1250
1427
|
fsyncDirectory(parent);
|
|
1251
|
-
committed = true;
|
|
1252
1428
|
} catch (error) {
|
|
1253
1429
|
failure = error;
|
|
1254
1430
|
} finally {
|
|
1255
1431
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1256
1432
|
}
|
|
1257
|
-
if (stagingIdentity) {
|
|
1258
|
-
if (!committed && stagingSnapshot) {
|
|
1259
|
-
try {
|
|
1260
|
-
const installed = captureManagedFileNoFollow(destination);
|
|
1261
|
-
if (
|
|
1262
|
-
installed.identity.dev === stagingSnapshot.identity.dev &&
|
|
1263
|
-
installed.identity.ino === stagingSnapshot.identity.ino &&
|
|
1264
|
-
installed.identity.sha256 === stagingSnapshot.identity.sha256
|
|
1265
|
-
) {
|
|
1266
|
-
const removed = exactUnlink(destination, {
|
|
1267
|
-
dev: installed.identity.dev,
|
|
1268
|
-
ino: installed.identity.ino,
|
|
1269
|
-
size: BigInt(installed.identity.size),
|
|
1270
|
-
mtimeNs: installed.identity.mtimeNs,
|
|
1271
|
-
sha256: installed.identity.sha256,
|
|
1272
|
-
quarantineName: `.skc-remove-${process.pid}-${randomUUID()}`,
|
|
1273
|
-
});
|
|
1274
|
-
if (!removed.ok) failure ??= new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
1275
|
-
}
|
|
1276
|
-
} catch (error) {
|
|
1277
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1433
|
+
if (stagingIdentity && outcome && mayCleanCurrentStaging(outcome)) {
|
|
1280
1434
|
try {
|
|
1281
1435
|
const named = fs.lstatSync(staging, { bigint: true });
|
|
1282
1436
|
if (named.dev !== stagingIdentity.dev || named.ino !== stagingIdentity.ino) {
|
|
@@ -1291,9 +1445,14 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1291
1445
|
}
|
|
1292
1446
|
|
|
1293
1447
|
/** Replace one managed regular file while retaining the secured staging fd through publication. */
|
|
1294
|
-
export function replaceManagedFileSync(
|
|
1448
|
+
export function replaceManagedFileSync(
|
|
1449
|
+
destination: string,
|
|
1450
|
+
bytes: Uint8Array,
|
|
1451
|
+
root: ManagedDirectoryRoot,
|
|
1452
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1453
|
+
): void {
|
|
1295
1454
|
const parent = path.dirname(destination);
|
|
1296
|
-
ensureManagedDirectory(parent, root);
|
|
1455
|
+
ensureManagedDirectory(parent, root, policy);
|
|
1297
1456
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.replacement`);
|
|
1298
1457
|
let fd: number | undefined;
|
|
1299
1458
|
let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
@@ -1338,8 +1497,9 @@ export async function replaceManagedFile(
|
|
|
1338
1497
|
destination: string,
|
|
1339
1498
|
bytes: Uint8Array,
|
|
1340
1499
|
root: ManagedDirectoryRoot,
|
|
1500
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1341
1501
|
): Promise<void> {
|
|
1342
|
-
replaceManagedFileSync(destination, bytes, root);
|
|
1502
|
+
replaceManagedFileSync(destination, bytes, root, policy);
|
|
1343
1503
|
}
|
|
1344
1504
|
|
|
1345
1505
|
/** Copy the exact bytes captured from one no-follow source descriptor. */
|
|
@@ -1348,11 +1508,12 @@ export async function copyManagedFileNoReplace(
|
|
|
1348
1508
|
destination: string,
|
|
1349
1509
|
snapshot = captureManagedFileNoFollow(source),
|
|
1350
1510
|
root?: ManagedDirectoryRoot,
|
|
1511
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1351
1512
|
): Promise<void> {
|
|
1352
1513
|
const named = captureManagedFileNoFollow(source);
|
|
1353
1514
|
if (!sameIdentity(snapshot.identity, named.identity) || !snapshot.bytes.equals(named.bytes))
|
|
1354
1515
|
throw new Error("source_changed");
|
|
1355
|
-
await publishManagedFileNoReplace(destination, snapshot.bytes, undefined, root);
|
|
1516
|
+
await publishManagedFileNoReplace(destination, snapshot.bytes, undefined, root, policy);
|
|
1356
1517
|
const destinationSnapshot = captureManagedFileNoFollow(destination);
|
|
1357
1518
|
if (!destinationSnapshot.bytes.equals(snapshot.bytes)) throw new Error("durability_failed");
|
|
1358
1519
|
}
|
|
@@ -1362,8 +1523,9 @@ export async function acquireManagedLock(
|
|
|
1362
1523
|
locksDirectory: string,
|
|
1363
1524
|
name: string,
|
|
1364
1525
|
root?: ManagedDirectoryRoot,
|
|
1526
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1365
1527
|
): Promise<ManagedStorageLock> {
|
|
1366
|
-
ensureManagedDirectory(locksDirectory, root);
|
|
1528
|
+
ensureManagedDirectory(locksDirectory, root, policy);
|
|
1367
1529
|
const lockPath = path.join(locksDirectory, `${name}.lock`);
|
|
1368
1530
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
1369
1531
|
let staleObservedAt: number | undefined;
|
|
@@ -1478,8 +1640,20 @@ export async function acquireManagedLock(
|
|
|
1478
1640
|
}
|
|
1479
1641
|
}
|
|
1480
1642
|
|
|
1643
|
+
export interface ManagedArtifactTreeLimits {
|
|
1644
|
+
maxFiles?: number;
|
|
1645
|
+
maxTotalBytes?: number;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1481
1648
|
/** Bounds a no-follow artifact tree before it can be copied or deleted. */
|
|
1482
|
-
export function validateManagedArtifactTree(root: string): void {
|
|
1649
|
+
export function validateManagedArtifactTree(root: string, limits: ManagedArtifactTreeLimits = {}): void {
|
|
1650
|
+
const clampLimit = (limit: number | undefined, maximum: number): number => {
|
|
1651
|
+
if (limit === undefined) return maximum;
|
|
1652
|
+
if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit <= 0) throw new Error("unsafe_artifacts");
|
|
1653
|
+
return Math.min(limit, maximum);
|
|
1654
|
+
};
|
|
1655
|
+
const maxFiles = clampLimit(limits.maxFiles, MANAGED_ARTIFACT_MAX_FILES);
|
|
1656
|
+
const maxTotalBytes = clampLimit(limits.maxTotalBytes, MANAGED_ARTIFACT_MAX_TOTAL_BYTES);
|
|
1483
1657
|
let files = 0;
|
|
1484
1658
|
let bytes = 0;
|
|
1485
1659
|
const visit = (directory: string, depth: number): void => {
|
|
@@ -1496,8 +1670,7 @@ export function validateManagedArtifactTree(root: string): void {
|
|
|
1496
1670
|
if (!stat.isFile() || stat.size > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("unsafe_artifacts");
|
|
1497
1671
|
files++;
|
|
1498
1672
|
bytes += stat.size;
|
|
1499
|
-
if (files >
|
|
1500
|
-
throw new Error("unsafe_artifacts");
|
|
1673
|
+
if (files > maxFiles || bytes > maxTotalBytes) throw new Error("artifact_capacity_exceeded");
|
|
1501
1674
|
}
|
|
1502
1675
|
};
|
|
1503
1676
|
const rootStat = fs.lstatSync(root);
|
|
@@ -1513,7 +1686,10 @@ export function fsyncManagedArtifactTree(root: string): NativeDirectoryTreeSnaps
|
|
|
1513
1686
|
const stat = fs.lstatSync(pathname);
|
|
1514
1687
|
if (stat.isSymbolicLink()) throw new Error("unsafe_artifacts");
|
|
1515
1688
|
if (stat.isFile()) {
|
|
1516
|
-
const fd = fs.openSync(
|
|
1689
|
+
const fd = fs.openSync(
|
|
1690
|
+
pathname,
|
|
1691
|
+
(process.platform === "win32" ? fs.constants.O_WRONLY : fs.constants.O_RDONLY) | fs.constants.O_NOFOLLOW,
|
|
1692
|
+
);
|
|
1517
1693
|
try {
|
|
1518
1694
|
fs.fsyncSync(fd);
|
|
1519
1695
|
} finally {
|