@gajae-code/coding-agent 0.17.1 → 0.17.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 +266 -0
- package/dist/types/async/job-manager.d.ts +7 -6
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/github-release.d.ts +17 -0
- package/dist/types/cli/public-command-entry.d.ts +56 -0
- package/dist/types/cli/public-command-errors.d.ts +126 -0
- package/dist/types/cli/public-command-evidence.d.ts +99 -0
- package/dist/types/cli/public-command-help.d.ts +65 -0
- package/dist/types/cli/public-command-registry.d.ts +69 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/cli/skills-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +4 -50
- package/dist/types/commands/sdk.d.ts +20 -37
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/commands/skills.d.ts +9 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +10 -1
- package/dist/types/config/model-profile-activation.d.ts +63 -4
- package/dist/types/config/model-registry.d.ts +20 -2
- package/dist/types/config/settings-schema.d.ts +11 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
- package/dist/types/coordinator-mcp/server.d.ts +3 -0
- package/dist/types/daemon/operator-contract.d.ts +5 -1
- package/dist/types/eval/js/worker-protocol.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +12 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
- package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/parse.d.ts +9 -1
- package/dist/types/main.d.ts +22 -0
- package/dist/types/modes/acp/acp-agent.d.ts +94 -1
- package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
- package/dist/types/modes/components/session-selector.d.ts +5 -2
- package/dist/types/modes/components/thinking-selector.d.ts +2 -1
- package/dist/types/modes/components/user-message-selector.d.ts +5 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/runtime/process-lifecycle.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +56 -0
- package/dist/types/runtime-mcp/pool.d.ts +17 -0
- package/dist/types/runtime-mcp/redaction.d.ts +20 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
- package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
- package/dist/types/sdk/acp/adapter.d.ts +27 -0
- package/dist/types/sdk/acp/final-text.d.ts +8 -0
- package/dist/types/sdk/broker/broker.d.ts +13 -2
- package/dist/types/sdk/broker/ensure.d.ts +46 -1
- package/dist/types/sdk/broker/identity.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
- package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/broker/session-index.d.ts +22 -3
- package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
- package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
- package/dist/types/sdk/bus/index.d.ts +19 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
- package/dist/types/sdk/cli/master-cli.d.ts +6 -10
- package/dist/types/sdk/cli/rows.d.ts +19 -1
- package/dist/types/sdk/cli/session-cli.d.ts +22 -5
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/guides/cli.d.ts +3 -4
- package/dist/types/sdk/guides/verify.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +9 -0
- package/dist/types/sdk/host/session-runtime.d.ts +31 -0
- package/dist/types/sdk/host/types.d.ts +10 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +38 -1
- package/dist/types/sdk/prompt-failure.d.ts +30 -1
- package/dist/types/sdk/prompt-status.d.ts +8 -0
- package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
- package/dist/types/sdk/router/replay-retention-gap-cache.d.ts +13 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +73 -2
- package/dist/types/sdk/transport/stdio.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +168 -21
- package/dist/types/session/fallback-chain-controller.d.ts +6 -6
- package/dist/types/session/fold-coordinator.d.ts +17 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
- package/dist/types/session/session-manager.d.ts +64 -1
- package/dist/types/session/session-work-lease.d.ts +14 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
- package/dist/types/setup/provider-onboarding.d.ts +81 -1
- package/dist/types/thinking.d.ts +3 -1
- package/dist/types/tools/bash.d.ts +37 -11
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/steer-fold.d.ts +31 -0
- package/dist/types/utils/git.d.ts +28 -2
- package/dist/types/utils/mupdf-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf-wasm-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf.d.ts +4 -0
- package/dist/types/utils/session-title-generation.d.ts +6 -0
- package/dist/types/utils/title-generator.d.ts +11 -2
- package/dist/types/web/scrapers/types.d.ts +3 -0
- package/package.json +18 -10
- package/scripts/build-binary.ts +9 -3
- package/scripts/compile-args.ts +0 -9
- package/scripts/embed-mupdf.ts +52 -0
- package/scripts/generate-sdk-operation-inventory.ts +31 -1
- package/scripts/vendor-markit.ts +196 -0
- package/src/async/job-manager.ts +60 -9
- package/src/cli/daemon-cli.ts +53 -18
- package/src/cli/github-release.ts +87 -5
- package/src/cli/mcp-cli.ts +38 -4
- package/src/cli/public-command-entry.ts +450 -0
- package/src/cli/public-command-errors.ts +581 -0
- package/src/cli/public-command-evidence.ts +717 -0
- package/src/cli/public-command-help.ts +360 -0
- package/src/cli/public-command-registry.ts +535 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +9 -1
- package/src/cli/skills-cli.ts +46 -2
- package/src/cli.ts +61 -1
- package/src/commands/daemon.ts +65 -69
- package/src/commands/sdk.ts +232 -100
- package/src/commands/setup.ts +2 -0
- package/src/commands/skills.ts +11 -0
- package/src/config/autorouting-tier-map.ts +16 -0
- package/src/config/file-lock.ts +199 -68
- package/src/config/model-profile-activation.ts +406 -53
- package/src/config/model-registry.ts +204 -25
- package/src/config/settings-schema.ts +26 -2
- package/src/coordinator-mcp/policy.ts +10 -5
- package/src/coordinator-mcp/question-gate-codec.ts +17 -0
- package/src/coordinator-mcp/question-state.ts +175 -8
- package/src/coordinator-mcp/server.ts +528 -87
- package/src/coordinator-mcp/session-reaper.ts +6 -4
- package/src/crash/record-loader.ts +6 -2
- package/src/daemon/operator-contract.ts +26 -12
- package/src/debug/report-bundle.ts +114 -24
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/context-manager.ts +8 -8
- package/src/eval/js/worker-core.ts +9 -4
- package/src/eval/js/worker-protocol.ts +2 -0
- package/src/extensibility/extensions/runner.ts +52 -4
- package/src/extensibility/extensions/types.ts +10 -0
- package/src/extensibility/gjc-plugins/registry.ts +196 -37
- package/src/extensibility/runtime-skill-discovery.ts +61 -3
- package/src/extensibility/shared-events.ts +3 -1
- package/src/gjc-runtime/managed-owner-admission.ts +67 -50
- package/src/gjc-runtime/session-state-lock.ts +165 -24
- package/src/gjc-runtime/ultragoal-succession.ts +31 -3
- package/src/internal-urls/docs-index.generated.ts +21 -19
- package/src/internal-urls/embedded-protocol.ts +83 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/parse.ts +21 -1
- package/src/internal-urls/router.ts +4 -2
- package/src/main.ts +123 -55
- package/src/modes/acp/acp-agent.ts +593 -47
- package/src/modes/acp/acp-event-mapper.ts +9 -0
- package/src/modes/acp/startup-provenance.ts +59 -0
- package/src/modes/components/custom-provider-wizard.ts +308 -7
- package/src/modes/components/model-selector.ts +31 -15
- package/src/modes/components/oauth-selector.ts +9 -0
- package/src/modes/components/session-selector.ts +47 -4
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/components/thinking-selector.ts +7 -3
- package/src/modes/components/user-message-selector.ts +57 -15
- package/src/modes/controllers/command-controller.ts +58 -32
- package/src/modes/controllers/extension-ui-controller.ts +33 -7
- package/src/modes/controllers/input-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +177 -18
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
- package/src/modes/types.ts +2 -2
- package/src/prompts/agent-fragments/browser-aside.md +1 -0
- package/src/prompts/system/title-system.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/read.md +2 -0
- package/src/runtime/network-prewarm-service.test.ts +36 -1
- package/src/runtime/network-prewarm-service.ts +28 -3
- package/src/runtime/process-lifecycle.ts +22 -11
- package/src/runtime-mcp/config.ts +16 -1
- package/src/runtime-mcp/manager.ts +689 -55
- package/src/runtime-mcp/pool.test.ts +31 -0
- package/src/runtime-mcp/pool.ts +304 -28
- package/src/runtime-mcp/redaction.ts +42 -0
- package/src/runtime-mcp/startup-policy.ts +7 -0
- package/src/runtime-mcp/tool-bridge.ts +71 -0
- package/src/runtime-mcp/transports/http.ts +108 -25
- package/src/sdk/acp/adapter.ts +90 -9
- package/src/sdk/acp/final-text.ts +19 -2
- package/src/sdk/broker/broker.ts +378 -52
- package/src/sdk/broker/ensure.ts +97 -19
- package/src/sdk/broker/identity.ts +9 -1
- package/src/sdk/broker/lifecycle-ledger.ts +95 -31
- package/src/sdk/broker/lifecycle.ts +690 -141
- package/src/sdk/broker/runtime.ts +1 -1
- package/src/sdk/broker/session-index.ts +97 -4
- package/src/sdk/broker/spawn-authority.ts +35 -7
- package/src/sdk/broker/stderr-drainer.ts +59 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/index.ts +721 -84
- package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -3
- package/src/sdk/cli/master-cli.ts +47 -36
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +687 -184
- package/src/sdk/client/client.ts +18 -0
- package/src/sdk/guides/catalog.ts +2 -2
- package/src/sdk/guides/cli.ts +20 -62
- package/src/sdk/guides/verify.ts +14 -0
- package/src/sdk/host/host.ts +49 -5
- package/src/sdk/host/session-runtime.test.ts +636 -9
- package/src/sdk/host/session-runtime.ts +499 -118
- package/src/sdk/host/types.ts +12 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
- package/src/sdk/host/websocket-transport.ts +40 -6
- package/src/sdk/index.ts +11 -0
- package/src/sdk/prompt-deadline-flush.ts +334 -0
- package/src/sdk/prompt-deadline-manager.ts +136 -4
- package/src/sdk/prompt-failure.ts +43 -1
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/prompt-tool-boundary.ts +121 -0
- package/src/sdk/protocol/operation-inventory.generated.json +106 -0
- package/src/sdk/router/replay-retention-gap-cache.ts +27 -0
- package/src/sdk/router/session-router.ts +31 -4
- package/src/sdk/session.ts +194 -16
- package/src/sdk/startup-capability.ts +1 -1
- package/src/sdk/transport/relay.ts +71 -6
- package/src/sdk/transport/serve-cli.ts +494 -49
- package/src/sdk/transport/socket.ts +12 -11
- package/src/sdk/transport/stdio.ts +7 -3
- package/src/session/agent-session.ts +2437 -400
- package/src/session/fallback-chain-controller.ts +6 -7
- package/src/session/fold-coordinator.ts +24 -5
- package/src/session/internal/managed-session-scope.ts +58 -18
- package/src/session/internal/managed-session-storage.ts +304 -11
- package/src/session/session-manager.ts +382 -44
- package/src/session/session-work-lease.ts +34 -0
- package/src/session-import/codex.ts +12 -3
- package/src/setup/model-onboarding-guidance.ts +1 -1
- package/src/setup/provider-onboarding.ts +397 -24
- package/src/slash-commands/builtin-registry.ts +258 -14
- package/src/task/index.ts +14 -2
- package/src/thinking.ts +21 -2
- package/src/tools/bash.ts +98 -78
- package/src/tools/fetch.ts +119 -20
- package/src/tools/job.ts +151 -25
- package/src/tools/output-meta.ts +15 -2
- package/src/tools/path-utils.ts +27 -3
- package/src/tools/read.ts +3 -3
- package/src/tools/steer-fold.ts +107 -0
- package/src/tools/tool-catalog.generated.ts +29 -2
- package/src/tools/tool-errors.ts +4 -0
- package/src/utils/git.ts +53 -3
- package/src/utils/markit.ts +55 -41
- package/src/utils/mupdf-assets.d.ts +4 -0
- package/src/utils/mupdf-embedded.ts +4 -0
- package/src/utils/mupdf-wasm-embedded.ts +3 -0
- package/src/utils/mupdf-wasm.ts +24 -2
- package/src/utils/mupdf.ts +129 -0
- package/src/utils/session-title-generation.ts +29 -0
- package/src/utils/title-generator.ts +46 -2
- package/src/web/scrapers/types.ts +24 -3
- package/vendor/markit-ai/LICENSE +21 -0
- package/vendor/markit-ai/MANIFEST.json +412 -0
- package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/config.js +139 -0
- package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
- package/vendor/markit-ai/dist/commands/convert.js +113 -0
- package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/formats.js +92 -0
- package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/init.js +29 -0
- package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/onboard.js +61 -0
- package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/plugin.js +58 -0
- package/vendor/markit-ai/dist/config.d.ts +26 -0
- package/vendor/markit-ai/dist/config.js +42 -0
- package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/audio.js +104 -0
- package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/csv.js +83 -0
- package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/docx.js +57 -0
- package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/epub.js +128 -0
- package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
- package/vendor/markit-ai/dist/converters/github.js +148 -0
- package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/html.js +30 -0
- package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/image.js +124 -0
- package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
- package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
- package/vendor/markit-ai/dist/converters/iwork.js +391 -0
- package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/json.js +21 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
- package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
- package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
- package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
- package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
- package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
- package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
- package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/pptx.js +289 -0
- package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
- package/vendor/markit-ai/dist/converters/rss.js +138 -0
- package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
- package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
- package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/xml.js +18 -0
- package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/yaml.js +17 -0
- package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/zip.js +57 -0
- package/vendor/markit-ai/dist/index.d.ts +30 -0
- package/vendor/markit-ai/dist/index.js +26 -0
- package/vendor/markit-ai/dist/main.d.ts +2 -0
- package/vendor/markit-ai/dist/main.js +191 -0
- package/vendor/markit-ai/dist/markit.d.ts +45 -0
- package/vendor/markit-ai/dist/markit.js +267 -0
- package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
- package/vendor/markit-ai/dist/plugins/api.js +42 -0
- package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
- package/vendor/markit-ai/dist/plugins/index.js +3 -0
- package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/installer.js +176 -0
- package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
- package/vendor/markit-ai/dist/plugins/loader.js +61 -0
- package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/types.js +1 -0
- package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
- package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
- package/vendor/markit-ai/dist/providers/index.js +56 -0
- package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/openai.js +76 -0
- package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
- package/vendor/markit-ai/dist/providers/types.js +1 -0
- package/vendor/markit-ai/dist/types.d.ts +36 -0
- package/vendor/markit-ai/dist/types.js +1 -0
- package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
- package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
- package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
- package/vendor/markit-ai/dist/utils/output.js +31 -0
- package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
- package/vendor/markit-ai/dist/utils/turndown.js +70 -0
- package/vendor/markit-ai/package.json +74 -0
- package/vendor/markit-ai.patch +49 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { constants, type Stats } from "node:fs";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
export const EVIDENCE_LIMITS = Object.freeze({
|
|
7
|
+
slots: 64,
|
|
8
|
+
recordBytes: 1_048_576,
|
|
9
|
+
totalBytes: 16_777_216,
|
|
10
|
+
ttlMs: 86_400_000,
|
|
11
|
+
fragmentBytes: 1024,
|
|
12
|
+
responseBytes: 8192,
|
|
13
|
+
});
|
|
14
|
+
export type EvidenceReferenceKind =
|
|
15
|
+
| "sessionId"
|
|
16
|
+
| "operationRef"
|
|
17
|
+
| "idempotencyKey"
|
|
18
|
+
| "claimId"
|
|
19
|
+
| "commandId"
|
|
20
|
+
| "turnId";
|
|
21
|
+
export interface EvidenceReference {
|
|
22
|
+
kind: EvidenceReferenceKind;
|
|
23
|
+
value: string;
|
|
24
|
+
}
|
|
25
|
+
export interface EvidenceClassification {
|
|
26
|
+
code: string;
|
|
27
|
+
category: "usage" | "unavailable" | "authorization" | "timeout" | "uncertain" | "operation" | "internal";
|
|
28
|
+
retryability: "yes" | "no" | "unknown";
|
|
29
|
+
outcomeCertainty: "not-applied" | "applied" | "unknown";
|
|
30
|
+
}
|
|
31
|
+
export type EvidenceUnavailableReason =
|
|
32
|
+
| "record_too_large"
|
|
33
|
+
| "quota_exceeded"
|
|
34
|
+
| "store_busy"
|
|
35
|
+
| "permission_denied"
|
|
36
|
+
| "permission_unsupported"
|
|
37
|
+
| "unsafe_path"
|
|
38
|
+
| "store_corrupt"
|
|
39
|
+
| "io_error"
|
|
40
|
+
| "verification_failed"
|
|
41
|
+
| "locator_too_large"
|
|
42
|
+
| "evidence_unavailable"
|
|
43
|
+
| "evidence_changed"
|
|
44
|
+
| "evidence_corrupt"
|
|
45
|
+
| "evidence_unsafe"
|
|
46
|
+
| "evidence_clock_invalid"
|
|
47
|
+
| "invalid_request";
|
|
48
|
+
export interface EvidenceUnavailable {
|
|
49
|
+
status: "unavailable";
|
|
50
|
+
reason: EvidenceUnavailableReason;
|
|
51
|
+
requiredBytes: number | null;
|
|
52
|
+
continuation: null;
|
|
53
|
+
}
|
|
54
|
+
export interface EvidenceContinuation {
|
|
55
|
+
kind: "local-store";
|
|
56
|
+
id: string;
|
|
57
|
+
sha256: string;
|
|
58
|
+
expiresAt: string;
|
|
59
|
+
page: number;
|
|
60
|
+
executable: "gjc";
|
|
61
|
+
argv: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface EvidenceLocatorOptions {
|
|
64
|
+
family: "sdk" | "daemon";
|
|
65
|
+
scopeAgentDir?: string;
|
|
66
|
+
json?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface PublishCommandEvidenceInput {
|
|
69
|
+
agentDir: string;
|
|
70
|
+
command: string[];
|
|
71
|
+
error: EvidenceClassification;
|
|
72
|
+
references: readonly EvidenceReference[];
|
|
73
|
+
}
|
|
74
|
+
export interface EvidenceRetained {
|
|
75
|
+
status: "retained";
|
|
76
|
+
id: string;
|
|
77
|
+
sha256: string;
|
|
78
|
+
bytes: number;
|
|
79
|
+
expiresAt: string;
|
|
80
|
+
continuation: EvidenceContinuation;
|
|
81
|
+
}
|
|
82
|
+
export interface EvidenceRecord {
|
|
83
|
+
schema: "gjc.command-error-record";
|
|
84
|
+
version: 1;
|
|
85
|
+
pagingVersion: 1;
|
|
86
|
+
id: string;
|
|
87
|
+
command: string[];
|
|
88
|
+
error: EvidenceClassification;
|
|
89
|
+
createdAt: string;
|
|
90
|
+
expiresAt: string;
|
|
91
|
+
references: EvidenceReference[];
|
|
92
|
+
}
|
|
93
|
+
export interface EvidencePage {
|
|
94
|
+
schema: "gjc.command-error-evidence";
|
|
95
|
+
version: 1;
|
|
96
|
+
ok: true;
|
|
97
|
+
id: string;
|
|
98
|
+
sha256: string;
|
|
99
|
+
expiresAt: string;
|
|
100
|
+
page: number;
|
|
101
|
+
complete: boolean;
|
|
102
|
+
fragments: { entryId: string; encoding: "base64"; offsetBytes: number; totalBytes: number; data: string }[];
|
|
103
|
+
next: EvidenceContinuation | null;
|
|
104
|
+
}
|
|
105
|
+
export interface ReadCommandEvidenceInput extends EvidenceLocatorOptions {
|
|
106
|
+
agentDir: string;
|
|
107
|
+
id: string;
|
|
108
|
+
sha256: string;
|
|
109
|
+
page?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type Handle = fs.FileHandle;
|
|
113
|
+
const kinds = new Set<string>(["sessionId", "operationRef", "idempotencyKey", "claimId", "commandId", "turnId"]);
|
|
114
|
+
const slots = Array.from({ length: EVIDENCE_LIMITS.slots }, (_, n) => `${String(n).padStart(2, "0")}.json`);
|
|
115
|
+
const idPattern = /^[a-f0-9]{32}$/;
|
|
116
|
+
const hashPattern = /^[a-f0-9]{64}$/;
|
|
117
|
+
class Failure extends Error {
|
|
118
|
+
constructor(readonly reason: EvidenceUnavailableReason) {
|
|
119
|
+
super(reason);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function fail(reason: EvidenceUnavailableReason): never {
|
|
123
|
+
throw new Failure(reason);
|
|
124
|
+
}
|
|
125
|
+
function code(error: unknown): string | undefined {
|
|
126
|
+
return (error as NodeJS.ErrnoException)?.code;
|
|
127
|
+
}
|
|
128
|
+
function unavailable(error: unknown, requiredBytes: number | null = null): EvidenceUnavailable {
|
|
129
|
+
const reason =
|
|
130
|
+
error instanceof Failure
|
|
131
|
+
? error.reason
|
|
132
|
+
: code(error) === "EACCES" || code(error) === "EPERM"
|
|
133
|
+
? "permission_denied"
|
|
134
|
+
: code(error) === "ELOOP" || code(error) === "ENOTDIR"
|
|
135
|
+
? "unsafe_path"
|
|
136
|
+
: "io_error";
|
|
137
|
+
return { status: "unavailable", reason, requiredBytes, continuation: null };
|
|
138
|
+
}
|
|
139
|
+
function identity(a: Stats, b: Stats): boolean {
|
|
140
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
141
|
+
}
|
|
142
|
+
function fileProof(stat: Stats, limit: number): void {
|
|
143
|
+
if (!stat.isFile() || stat.uid !== process.getuid!() || (stat.mode & 0o7777) !== 0o600 || stat.nlink !== 1)
|
|
144
|
+
fail("unsafe_path");
|
|
145
|
+
if (stat.size > limit || stat.size < 0) fail("store_corrupt");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Each ancestor is protected against replacement by other users before a child
|
|
149
|
+
// is opened. A root-owned sticky directory protects a current-user-owned child.
|
|
150
|
+
// Same-user compromise is outside this permission boundary. No realpath fallback.
|
|
151
|
+
async function openStore(
|
|
152
|
+
agentDir: string,
|
|
153
|
+
create: boolean,
|
|
154
|
+
): Promise<{ root: string; verify(): Promise<void>; close(): Promise<void>; directory: Handle }> {
|
|
155
|
+
if (process.platform === "win32" || !process.getuid || !constants.O_NOFOLLOW || !constants.O_DIRECTORY)
|
|
156
|
+
fail("permission_unsupported");
|
|
157
|
+
const root = path.join(path.resolve(agentDir), "cli-error-evidence-v1");
|
|
158
|
+
const handles: { name: string; handle: Handle; stat: Stats }[] = [];
|
|
159
|
+
try {
|
|
160
|
+
const components = root.split(path.sep).filter(Boolean);
|
|
161
|
+
let current = path.parse(root).root;
|
|
162
|
+
let stickyParent = false;
|
|
163
|
+
for (let i = -1; i < components.length; i++) {
|
|
164
|
+
if (i >= 0) current = path.join(current, components[i]!);
|
|
165
|
+
if (i === components.length - 1 && create) {
|
|
166
|
+
try {
|
|
167
|
+
await fs.mkdir(current, { mode: 0o700 });
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (code(error) !== "EEXIST") throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const handle = await fs.open(current, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
173
|
+
const stat = await handle.stat();
|
|
174
|
+
handles.push({ name: current, handle, stat });
|
|
175
|
+
if (!stat.isDirectory() || (stat.uid !== 0 && stat.uid !== process.getuid())) fail("unsafe_path");
|
|
176
|
+
if (stickyParent && stat.uid !== process.getuid()) fail("unsafe_path");
|
|
177
|
+
const writable = (stat.mode & 0o022) !== 0;
|
|
178
|
+
const sticky = stat.uid === 0 && (stat.mode & 0o1000) !== 0;
|
|
179
|
+
if (writable && !sticky) fail("unsafe_path");
|
|
180
|
+
stickyParent = writable;
|
|
181
|
+
if (i === components.length - 1 && (stat.uid !== process.getuid() || (stat.mode & 0o7777) !== 0o700))
|
|
182
|
+
fail("unsafe_path");
|
|
183
|
+
}
|
|
184
|
+
const verify = async () => {
|
|
185
|
+
for (const item of handles) {
|
|
186
|
+
const live = await fs.lstat(item.name);
|
|
187
|
+
const opened = await item.handle.stat();
|
|
188
|
+
if (
|
|
189
|
+
!identity(live, item.stat) ||
|
|
190
|
+
!identity(opened, item.stat) ||
|
|
191
|
+
live.mode !== item.stat.mode ||
|
|
192
|
+
live.uid !== item.stat.uid ||
|
|
193
|
+
!live.isDirectory()
|
|
194
|
+
)
|
|
195
|
+
fail("unsafe_path");
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
await verify();
|
|
199
|
+
return {
|
|
200
|
+
root,
|
|
201
|
+
verify,
|
|
202
|
+
directory: handles[handles.length - 1]!.handle,
|
|
203
|
+
close: async () => {
|
|
204
|
+
await Promise.all(handles.map(item => item.handle.close()));
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
await Promise.all(handles.map(item => item.handle.close().catch(() => {})));
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type Store = Awaited<ReturnType<typeof openStore>>;
|
|
214
|
+
async function readFile(
|
|
215
|
+
store: Store,
|
|
216
|
+
name: string,
|
|
217
|
+
limit: number = EVIDENCE_LIMITS.recordBytes,
|
|
218
|
+
): Promise<{ bytes: Buffer; stat: Stats } | null> {
|
|
219
|
+
await store.verify();
|
|
220
|
+
let handle: Handle;
|
|
221
|
+
try {
|
|
222
|
+
handle = await fs.open(
|
|
223
|
+
path.join(store.root, name),
|
|
224
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
225
|
+
);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (code(error) === "ENOENT") return null;
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const before = await handle.stat();
|
|
232
|
+
fileProof(before, limit);
|
|
233
|
+
const bytes = Buffer.alloc(before.size);
|
|
234
|
+
let offset = 0;
|
|
235
|
+
while (offset < bytes.length) {
|
|
236
|
+
const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
237
|
+
if (!bytesRead) fail("verification_failed");
|
|
238
|
+
offset += bytesRead;
|
|
239
|
+
}
|
|
240
|
+
const after = await handle.stat();
|
|
241
|
+
fileProof(after, limit);
|
|
242
|
+
const named = await fs.lstat(path.join(store.root, name));
|
|
243
|
+
if (
|
|
244
|
+
!identity(before, after) ||
|
|
245
|
+
!identity(after, named) ||
|
|
246
|
+
before.size !== after.size ||
|
|
247
|
+
before.mtimeMs !== after.mtimeMs ||
|
|
248
|
+
before.ctimeMs !== after.ctimeMs
|
|
249
|
+
)
|
|
250
|
+
fail("verification_failed");
|
|
251
|
+
fileProof(named, limit);
|
|
252
|
+
await store.verify();
|
|
253
|
+
return { bytes, stat: after };
|
|
254
|
+
} finally {
|
|
255
|
+
await handle.close();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function validClassification(error: EvidenceClassification): boolean {
|
|
260
|
+
return (
|
|
261
|
+
!!error &&
|
|
262
|
+
typeof error.code === "string" &&
|
|
263
|
+
/^[a-z][a-z0-9_]{0,95}$/.test(error.code) &&
|
|
264
|
+
["usage", "unavailable", "authorization", "timeout", "uncertain", "operation", "internal"].includes(
|
|
265
|
+
error.category,
|
|
266
|
+
) &&
|
|
267
|
+
["yes", "no", "unknown"].includes(error.retryability) &&
|
|
268
|
+
["not-applied", "applied", "unknown"].includes(error.outcomeCertainty)
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
function validCommand(command: string[]): boolean {
|
|
272
|
+
return (
|
|
273
|
+
Array.isArray(command) &&
|
|
274
|
+
command.length > 0 &&
|
|
275
|
+
command.length <= 5 &&
|
|
276
|
+
["sdk", "daemon"].includes(command[0]!) &&
|
|
277
|
+
command.every(token => typeof token === "string" && /^[a-z][a-z-]{0,31}$/.test(token))
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
function recordFromBytes(bytes: Buffer): EvidenceRecord {
|
|
281
|
+
let value: EvidenceRecord;
|
|
282
|
+
try {
|
|
283
|
+
value = JSON.parse(bytes.toString("utf8"));
|
|
284
|
+
} catch {
|
|
285
|
+
return fail("store_corrupt");
|
|
286
|
+
}
|
|
287
|
+
if (
|
|
288
|
+
value?.schema !== "gjc.command-error-record" ||
|
|
289
|
+
value.version !== 1 ||
|
|
290
|
+
value.pagingVersion !== 1 ||
|
|
291
|
+
!idPattern.test(value.id) ||
|
|
292
|
+
!validCommand(value.command) ||
|
|
293
|
+
!validClassification(value.error) ||
|
|
294
|
+
!Array.isArray(value.references) ||
|
|
295
|
+
!value.references.every(
|
|
296
|
+
ref => ref && kinds.has(ref.kind) && typeof ref.value === "string" && Object.keys(ref).length === 2,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
fail("store_corrupt");
|
|
300
|
+
const created = Date.parse(value.createdAt);
|
|
301
|
+
const expires = Date.parse(value.expiresAt);
|
|
302
|
+
if (
|
|
303
|
+
!Number.isFinite(created) ||
|
|
304
|
+
!Number.isFinite(expires) ||
|
|
305
|
+
expires - created !== EVIDENCE_LIMITS.ttlMs ||
|
|
306
|
+
new Date(created).toISOString() !== value.createdAt ||
|
|
307
|
+
new Date(expires).toISOString() !== value.expiresAt
|
|
308
|
+
)
|
|
309
|
+
fail("store_corrupt");
|
|
310
|
+
if (created > Date.now()) fail("evidence_clock_invalid");
|
|
311
|
+
const canonical = serializeRecord(value);
|
|
312
|
+
if (!canonical.equals(bytes)) fail("store_corrupt");
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Count JSON-escaped UTF-8 bytes without allocating a copy of an oversized value.
|
|
317
|
+
function stringBytes(value: string): number {
|
|
318
|
+
let size = 2;
|
|
319
|
+
for (let i = 0; i < value.length; i++) {
|
|
320
|
+
const c = value.charCodeAt(i);
|
|
321
|
+
if (c === 34 || c === 92 || c === 8 || c === 9 || c === 10 || c === 12 || c === 13) size += 2;
|
|
322
|
+
else if (c < 32) size += 6;
|
|
323
|
+
else if (c < 128) size++;
|
|
324
|
+
else if (c < 2048) size += 2;
|
|
325
|
+
else if (
|
|
326
|
+
c >= 0xd800 &&
|
|
327
|
+
c <= 0xdbff &&
|
|
328
|
+
i + 1 < value.length &&
|
|
329
|
+
value.charCodeAt(i + 1) >= 0xdc00 &&
|
|
330
|
+
value.charCodeAt(i + 1) <= 0xdfff
|
|
331
|
+
) {
|
|
332
|
+
size += 4;
|
|
333
|
+
i++;
|
|
334
|
+
} else if (c >= 0xd800 && c <= 0xdfff) size += 6;
|
|
335
|
+
else size += 3;
|
|
336
|
+
if (size > EVIDENCE_LIMITS.recordBytes) fail("record_too_large");
|
|
337
|
+
}
|
|
338
|
+
return size;
|
|
339
|
+
}
|
|
340
|
+
function serializeRecord(record: EvidenceRecord): Buffer {
|
|
341
|
+
if (!validCommand(record.command) || !validClassification(record.error)) fail("invalid_request");
|
|
342
|
+
let referenceSize = 2;
|
|
343
|
+
for (const ref of record.references) {
|
|
344
|
+
if (!ref || !kinds.has(ref.kind) || typeof ref.value !== "string") fail("invalid_request");
|
|
345
|
+
referenceSize += (referenceSize === 2 ? 0 : 1) + 18 + stringBytes(ref.kind) + stringBytes(ref.value);
|
|
346
|
+
if (referenceSize > EVIDENCE_LIMITS.recordBytes) fail("record_too_large");
|
|
347
|
+
}
|
|
348
|
+
// Only explicitly selected fields are serialized, never caller objects or details.
|
|
349
|
+
const safe = {
|
|
350
|
+
schema: record.schema,
|
|
351
|
+
version: record.version,
|
|
352
|
+
pagingVersion: record.pagingVersion,
|
|
353
|
+
id: record.id,
|
|
354
|
+
command: record.command,
|
|
355
|
+
error: {
|
|
356
|
+
code: record.error.code,
|
|
357
|
+
category: record.error.category,
|
|
358
|
+
retryability: record.error.retryability,
|
|
359
|
+
outcomeCertainty: record.error.outcomeCertainty,
|
|
360
|
+
},
|
|
361
|
+
createdAt: record.createdAt,
|
|
362
|
+
expiresAt: record.expiresAt,
|
|
363
|
+
references: [] as EvidenceReference[],
|
|
364
|
+
};
|
|
365
|
+
if (Buffer.byteLength(JSON.stringify(safe)) - 2 + referenceSize + 1 > EVIDENCE_LIMITS.recordBytes)
|
|
366
|
+
fail("record_too_large");
|
|
367
|
+
safe.references = record.references.map(ref => ({ kind: ref.kind, value: ref.value }));
|
|
368
|
+
const bytes = Buffer.from(`${JSON.stringify(safe)}\n`);
|
|
369
|
+
if (bytes.length > EVIDENCE_LIMITS.recordBytes) fail("record_too_large");
|
|
370
|
+
return bytes;
|
|
371
|
+
}
|
|
372
|
+
function digest(bytes: Buffer): string {
|
|
373
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
374
|
+
}
|
|
375
|
+
function continuation(
|
|
376
|
+
record: EvidenceRecord,
|
|
377
|
+
sha256: string,
|
|
378
|
+
page: number,
|
|
379
|
+
options: EvidenceLocatorOptions,
|
|
380
|
+
): EvidenceContinuation {
|
|
381
|
+
if (
|
|
382
|
+
options.scopeAgentDir !== undefined &&
|
|
383
|
+
(options.scopeAgentDir.length > EVIDENCE_LIMITS.responseBytes || options.scopeAgentDir.includes("\0"))
|
|
384
|
+
)
|
|
385
|
+
fail("locator_too_large");
|
|
386
|
+
const argv = [options.family, "--error-ref", record.id, "--error-sha256", sha256, "--error-page", String(page)];
|
|
387
|
+
if (options.scopeAgentDir !== undefined) argv.push(`--error-agent-dir=${options.scopeAgentDir}`);
|
|
388
|
+
if (options.json) argv.push("--json");
|
|
389
|
+
return { kind: "local-store", id: record.id, sha256, expiresAt: record.expiresAt, page, executable: "gjc", argv };
|
|
390
|
+
}
|
|
391
|
+
function pageFor(
|
|
392
|
+
record: EvidenceRecord,
|
|
393
|
+
bytes: Buffer,
|
|
394
|
+
sha256: string,
|
|
395
|
+
page: number,
|
|
396
|
+
options: EvidenceLocatorOptions,
|
|
397
|
+
): EvidencePage {
|
|
398
|
+
const offset = (page - 1) * EVIDENCE_LIMITS.fragmentBytes;
|
|
399
|
+
if (!Number.isSafeInteger(page) || page < 1 || offset >= bytes.length) fail("invalid_request");
|
|
400
|
+
const end = Math.min(offset + EVIDENCE_LIMITS.fragmentBytes, bytes.length);
|
|
401
|
+
const result: EvidencePage = {
|
|
402
|
+
schema: "gjc.command-error-evidence",
|
|
403
|
+
version: 1,
|
|
404
|
+
ok: true,
|
|
405
|
+
id: record.id,
|
|
406
|
+
sha256,
|
|
407
|
+
expiresAt: record.expiresAt,
|
|
408
|
+
page,
|
|
409
|
+
complete: end === bytes.length,
|
|
410
|
+
fragments: [
|
|
411
|
+
{
|
|
412
|
+
entryId: "record",
|
|
413
|
+
encoding: "base64",
|
|
414
|
+
offsetBytes: offset,
|
|
415
|
+
totalBytes: bytes.length,
|
|
416
|
+
data: bytes.subarray(offset, end).toString("base64"),
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
next: end === bytes.length ? null : continuation(record, sha256, page + 1, options),
|
|
420
|
+
};
|
|
421
|
+
if (Buffer.byteLength(`${JSON.stringify(result)}\n`) > EVIDENCE_LIMITS.responseBytes) fail("locator_too_large");
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
async function removeOwned(store: Store, name: string, stat: Stats): Promise<void> {
|
|
425
|
+
await store.verify();
|
|
426
|
+
const current = await fs.lstat(path.join(store.root, name));
|
|
427
|
+
if (!identity(current, stat)) fail("unsafe_path");
|
|
428
|
+
await fs.unlink(path.join(store.root, name));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Publisher lock ownership record. Publication is exclusive, so the lock must survive a
|
|
433
|
+
* publisher that dies mid-publish without wedging the store forever. Reclamation is
|
|
434
|
+
* therefore bounded and evidence-backed: the lock is released only when its recorded
|
|
435
|
+
* owner is provably gone (ESRCH), or when an incomplete record proves a crash inside the
|
|
436
|
+
* create-to-write window and the bounded grace has elapsed.
|
|
437
|
+
*
|
|
438
|
+
* A recorded pid that is still alive is never reclaimed, even after the grace window:
|
|
439
|
+
* a hung publisher is indistinguishable from a live one, and stealing its lock is the
|
|
440
|
+
* only way two publishers could enter concurrently. The residual failure mode is a
|
|
441
|
+
* reused pid wedging the store, which fails closed as `store_busy` rather than
|
|
442
|
+
* corrupting it.
|
|
443
|
+
*/
|
|
444
|
+
const LOCK_SCHEMA = "gjc.command-error-lock";
|
|
445
|
+
const LOCK_RECORD_BYTES = 512;
|
|
446
|
+
const LOCK_GRACE_MS = 5_000;
|
|
447
|
+
const LOCK_ATTEMPTS = 2;
|
|
448
|
+
|
|
449
|
+
interface EvidenceLockRecord {
|
|
450
|
+
schema: typeof LOCK_SCHEMA;
|
|
451
|
+
version: 1;
|
|
452
|
+
pid: number;
|
|
453
|
+
createdAt: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function lockRecordBytes(): Buffer {
|
|
457
|
+
const record: EvidenceLockRecord = {
|
|
458
|
+
schema: LOCK_SCHEMA,
|
|
459
|
+
version: 1,
|
|
460
|
+
pid: process.pid,
|
|
461
|
+
createdAt: new Date().toISOString(),
|
|
462
|
+
};
|
|
463
|
+
return Buffer.from(`${JSON.stringify(record)}\n`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function lockRecordFromBytes(bytes: Buffer): EvidenceLockRecord | undefined {
|
|
467
|
+
let value: EvidenceLockRecord;
|
|
468
|
+
try {
|
|
469
|
+
value = JSON.parse(bytes.toString("utf8")) as EvidenceLockRecord;
|
|
470
|
+
} catch {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
if (value?.schema !== LOCK_SCHEMA || value.version !== 1) return undefined;
|
|
474
|
+
if (!Number.isSafeInteger(value.pid) || value.pid <= 0) return undefined;
|
|
475
|
+
if (typeof value.createdAt !== "string" || !Number.isFinite(Date.parse(value.createdAt))) return undefined;
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** ESRCH proves the recorded owner is gone; EPERM means it is alive but owned elsewhere. */
|
|
480
|
+
function lockOwnerAlive(pid: number): boolean {
|
|
481
|
+
try {
|
|
482
|
+
process.kill(pid, 0);
|
|
483
|
+
return true;
|
|
484
|
+
} catch (error) {
|
|
485
|
+
return code(error) !== "ESRCH";
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function lockAbandoned(existing: { bytes: Buffer; stat: Stats }, now: number): boolean {
|
|
490
|
+
const record = lockRecordFromBytes(existing.bytes);
|
|
491
|
+
if (!record) return now - existing.stat.mtimeMs >= LOCK_GRACE_MS;
|
|
492
|
+
return !lockOwnerAlive(record.pid);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Exactly one publisher may hold the lock: the exclusive create is the admission
|
|
497
|
+
* primitive, and a reclaim is an identity-verified unlink of a specifically abandoned
|
|
498
|
+
* inode followed by another exclusive create. A concurrent reclaimer that loses either
|
|
499
|
+
* step observes the successor's different inode and reports `store_busy` instead of
|
|
500
|
+
* deleting a lock it does not own.
|
|
501
|
+
*/
|
|
502
|
+
async function acquirePublicationLock(store: Store): Promise<Handle> {
|
|
503
|
+
for (let attempt = 1; ; attempt++) {
|
|
504
|
+
try {
|
|
505
|
+
return await fs.open(
|
|
506
|
+
path.join(store.root, "lock"),
|
|
507
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
508
|
+
0o600,
|
|
509
|
+
);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (code(error) !== "EEXIST") throw error;
|
|
512
|
+
if (attempt >= LOCK_ATTEMPTS) fail("store_busy");
|
|
513
|
+
const existing = await readFile(store, "lock", LOCK_RECORD_BYTES);
|
|
514
|
+
if (!existing || !lockAbandoned(existing, Date.now())) fail("store_busy");
|
|
515
|
+
try {
|
|
516
|
+
await removeOwned(store, "lock", existing.stat);
|
|
517
|
+
} catch {
|
|
518
|
+
fail("store_busy");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async function checkLayout(store: Store): Promise<void> {
|
|
524
|
+
const directory = await fs.opendir(store.root);
|
|
525
|
+
let count = 0;
|
|
526
|
+
for await (const entry of directory) {
|
|
527
|
+
if (++count > 66 || (!slots.includes(entry.name) && entry.name !== "pending" && entry.name !== "lock"))
|
|
528
|
+
fail("store_corrupt");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Narrow per-call fault instrumentation; production callers omit this argument. */
|
|
533
|
+
export interface EvidencePublicationTestHooks {
|
|
534
|
+
id?: string;
|
|
535
|
+
before?: (phase: "write" | "file-sync" | "link" | "directory-sync" | "readback" | "cleanup") => void | Promise<void>;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export async function publishCommandEvidence(
|
|
539
|
+
input: PublishCommandEvidenceInput,
|
|
540
|
+
options: EvidenceLocatorOptions,
|
|
541
|
+
testHooks?: EvidencePublicationTestHooks,
|
|
542
|
+
): Promise<EvidenceRetained | EvidenceUnavailable> {
|
|
543
|
+
let store: Store | undefined;
|
|
544
|
+
let lock: Handle | undefined;
|
|
545
|
+
let lockStat: Stats | undefined;
|
|
546
|
+
let result: EvidenceRetained | EvidenceUnavailable;
|
|
547
|
+
let requiredBytes: number | null = null;
|
|
548
|
+
try {
|
|
549
|
+
if (!validCommand(input.command) || input.command[0] !== options.family) fail("invalid_request");
|
|
550
|
+
const now = Date.now();
|
|
551
|
+
const record: EvidenceRecord = {
|
|
552
|
+
schema: "gjc.command-error-record",
|
|
553
|
+
version: 1,
|
|
554
|
+
pagingVersion: 1,
|
|
555
|
+
id: testHooks?.id ?? randomBytes(16).toString("hex"),
|
|
556
|
+
command: input.command,
|
|
557
|
+
error: input.error,
|
|
558
|
+
createdAt: new Date(now).toISOString(),
|
|
559
|
+
expiresAt: new Date(now + EVIDENCE_LIMITS.ttlMs).toISOString(),
|
|
560
|
+
references: input.references as EvidenceReference[],
|
|
561
|
+
};
|
|
562
|
+
if (!idPattern.test(record.id)) fail("invalid_request");
|
|
563
|
+
const bytes = serializeRecord(record);
|
|
564
|
+
requiredBytes = bytes.length;
|
|
565
|
+
const sha256 = digest(bytes);
|
|
566
|
+
const locator = continuation(record, sha256, 1, options);
|
|
567
|
+
if (Buffer.byteLength(JSON.stringify(locator)) > 4096) fail("locator_too_large");
|
|
568
|
+
pageFor(record, bytes, sha256, Math.max(1, Math.ceil(bytes.length / 1024) - 1), options);
|
|
569
|
+
store = await openStore(input.agentDir, true);
|
|
570
|
+
lock = await acquirePublicationLock(store);
|
|
571
|
+
await lock.writeFile(lockRecordBytes());
|
|
572
|
+
await lock.sync();
|
|
573
|
+
lockStat = await lock.stat();
|
|
574
|
+
fileProof(lockStat, LOCK_RECORD_BYTES);
|
|
575
|
+
await checkLayout(store);
|
|
576
|
+
const pending = await readFile(store, "pending");
|
|
577
|
+
if (pending) await removeOwned(store, "pending", pending.stat);
|
|
578
|
+
let total = 0;
|
|
579
|
+
let empty: string | undefined;
|
|
580
|
+
for (const name of slots) {
|
|
581
|
+
const existing = await readFile(store, name);
|
|
582
|
+
if (!existing) {
|
|
583
|
+
empty ??= name;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const old = recordFromBytes(existing.bytes);
|
|
587
|
+
if (old.id === record.id) fail("verification_failed");
|
|
588
|
+
if (Date.parse(old.expiresAt) <= now) {
|
|
589
|
+
await removeOwned(store, name, existing.stat);
|
|
590
|
+
empty ??= name;
|
|
591
|
+
} else total += existing.bytes.length;
|
|
592
|
+
}
|
|
593
|
+
if (!empty || total + bytes.length > EVIDENCE_LIMITS.totalBytes) fail("quota_exceeded");
|
|
594
|
+
await store.verify();
|
|
595
|
+
const pendingHandle = await fs.open(
|
|
596
|
+
path.join(store.root, "pending"),
|
|
597
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
598
|
+
0o600,
|
|
599
|
+
);
|
|
600
|
+
let pendingStat: Stats;
|
|
601
|
+
try {
|
|
602
|
+
fileProof(await pendingHandle.stat(), EVIDENCE_LIMITS.recordBytes);
|
|
603
|
+
await testHooks?.before?.("write");
|
|
604
|
+
await pendingHandle.writeFile(bytes);
|
|
605
|
+
await testHooks?.before?.("file-sync");
|
|
606
|
+
await pendingHandle.sync();
|
|
607
|
+
pendingStat = await pendingHandle.stat();
|
|
608
|
+
fileProof(pendingStat, EVIDENCE_LIMITS.recordBytes);
|
|
609
|
+
} finally {
|
|
610
|
+
await pendingHandle.close();
|
|
611
|
+
}
|
|
612
|
+
await store.verify();
|
|
613
|
+
if (!identity(await fs.lstat(path.join(store.root, "pending")), pendingStat)) fail("verification_failed");
|
|
614
|
+
// link is atomic and never replaces an occupied slot; rename is not safe here.
|
|
615
|
+
await testHooks?.before?.("link");
|
|
616
|
+
await fs.link(path.join(store.root, "pending"), path.join(store.root, empty));
|
|
617
|
+
await removeOwned(store, "pending", pendingStat);
|
|
618
|
+
await testHooks?.before?.("directory-sync");
|
|
619
|
+
await store.directory.sync();
|
|
620
|
+
await testHooks?.before?.("readback");
|
|
621
|
+
const verified = await readFile(store, empty);
|
|
622
|
+
if (
|
|
623
|
+
!verified ||
|
|
624
|
+
!identity(verified.stat, pendingStat) ||
|
|
625
|
+
!verified.bytes.equals(bytes) ||
|
|
626
|
+
digest(verified.bytes) !== sha256
|
|
627
|
+
)
|
|
628
|
+
fail("verification_failed");
|
|
629
|
+
recordFromBytes(verified.bytes);
|
|
630
|
+
result = {
|
|
631
|
+
status: "retained",
|
|
632
|
+
id: record.id,
|
|
633
|
+
sha256,
|
|
634
|
+
bytes: bytes.length,
|
|
635
|
+
expiresAt: record.expiresAt,
|
|
636
|
+
continuation: locator,
|
|
637
|
+
};
|
|
638
|
+
} catch (error) {
|
|
639
|
+
result = unavailable(error, requiredBytes);
|
|
640
|
+
} finally {
|
|
641
|
+
if (store && lock && lockStat) {
|
|
642
|
+
try {
|
|
643
|
+
await testHooks?.before?.("cleanup");
|
|
644
|
+
await removeOwned(store, "lock", lockStat);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
result = unavailable(error, requiredBytes);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
await lock?.close();
|
|
651
|
+
await store?.close();
|
|
652
|
+
} catch (error) {
|
|
653
|
+
result = unavailable(error, requiredBytes);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return result!;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export async function readCommandEvidence(
|
|
660
|
+
input: ReadCommandEvidenceInput,
|
|
661
|
+
): Promise<{ status: "available"; page: EvidencePage } | EvidenceUnavailable> {
|
|
662
|
+
let store: Store | undefined;
|
|
663
|
+
let result: { status: "available"; page: EvidencePage } | EvidenceUnavailable;
|
|
664
|
+
try {
|
|
665
|
+
const page = input.page ?? 1;
|
|
666
|
+
if (
|
|
667
|
+
!idPattern.test(input.id) ||
|
|
668
|
+
!hashPattern.test(input.sha256) ||
|
|
669
|
+
!["sdk", "daemon"].includes(input.family) ||
|
|
670
|
+
!Number.isSafeInteger(page) ||
|
|
671
|
+
page < 1 ||
|
|
672
|
+
page > 1024
|
|
673
|
+
)
|
|
674
|
+
fail("invalid_request");
|
|
675
|
+
try {
|
|
676
|
+
store = await openStore(input.agentDir, false);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if (code(error) === "ENOENT") fail("evidence_unavailable");
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
await checkLayout(store);
|
|
682
|
+
await readFile(store, "lock", LOCK_RECORD_BYTES);
|
|
683
|
+
await readFile(store, "pending");
|
|
684
|
+
let match: { record: EvidenceRecord; bytes: Buffer } | undefined;
|
|
685
|
+
let total = 0;
|
|
686
|
+
for (const name of slots) {
|
|
687
|
+
const existing = await readFile(store, name);
|
|
688
|
+
if (!existing) continue;
|
|
689
|
+
total += existing.bytes.length;
|
|
690
|
+
if (total > EVIDENCE_LIMITS.totalBytes) fail("store_corrupt");
|
|
691
|
+
const record = recordFromBytes(existing.bytes);
|
|
692
|
+
if (record.id !== input.id) continue;
|
|
693
|
+
if (match) fail("store_corrupt");
|
|
694
|
+
if (record.command[0] !== input.family || Date.parse(record.expiresAt) <= Date.now())
|
|
695
|
+
fail("evidence_unavailable");
|
|
696
|
+
if (digest(existing.bytes) !== input.sha256) fail("evidence_changed");
|
|
697
|
+
match = { record, bytes: existing.bytes };
|
|
698
|
+
}
|
|
699
|
+
if (!match) fail("evidence_unavailable");
|
|
700
|
+
if (Date.parse(match.record.createdAt) > Date.now()) fail("evidence_clock_invalid");
|
|
701
|
+
if (Date.parse(match.record.expiresAt) <= Date.now()) fail("evidence_unavailable");
|
|
702
|
+
await store.verify();
|
|
703
|
+
result = { status: "available", page: pageFor(match.record, match.bytes, input.sha256, page, input) };
|
|
704
|
+
} catch (error) {
|
|
705
|
+
result = unavailable(error);
|
|
706
|
+
if (result.reason === "unsafe_path") result.reason = "evidence_unsafe";
|
|
707
|
+
if (result.reason === "store_corrupt") result.reason = "evidence_corrupt";
|
|
708
|
+
if (result.reason === "verification_failed") result.reason = "evidence_changed";
|
|
709
|
+
} finally {
|
|
710
|
+
try {
|
|
711
|
+
await store?.close();
|
|
712
|
+
} catch (error) {
|
|
713
|
+
result = unavailable(error);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return result!;
|
|
717
|
+
}
|