@hraness/ghostget 0.17.2
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 +458 -0
- package/LICENSE +21 -0
- package/README.md +1359 -0
- package/bunfig.toml +2 -0
- package/dist/apple-photos-client.js +704 -0
- package/dist/beeper-client.js +4602 -0
- package/dist/client.js +1605 -0
- package/dist/index-26yq8q16.js +31 -0
- package/dist/index-9t13b4k7.js +508 -0
- package/dist/index-axwm9b3v.js +5 -0
- package/dist/index-gwk7rbyj.js +30 -0
- package/dist/index.js +701 -0
- package/dist/messaging.js +1130 -0
- package/dist/omni-client.js +1406 -0
- package/dist/whatsapp-client.js +355 -0
- package/docs/imessage-direct-provider.md +106 -0
- package/docs/rental-listings.md +94 -0
- package/package.json +600 -0
- package/skills/ghostget/SKILL.md +251 -0
- package/skills/ghostget/agents/openai.yaml +4 -0
- package/skills/ghostget/references/adapter-contract.md +181 -0
- package/skills/ghostget/references/apple-photos.md +115 -0
- package/skills/ghostget/references/article-drafts.md +235 -0
- package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
- package/skills/ghostget/references/cross-posting-video.md +139 -0
- package/skills/ghostget/references/cross-posting.md +82 -0
- package/skills/ghostget/references/derivation.md +390 -0
- package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
- package/skills/ghostget/references/install.md +66 -0
- package/skills/ghostget/references/linkedin-adapter.md +442 -0
- package/skills/ghostget/references/messaging.md +168 -0
- package/skills/ghostget/references/meta-comet-contract.md +153 -0
- package/skills/ghostget/references/platform-patterns.md +122 -0
- package/skills/ghostget/references/provider-plugins.md +496 -0
- package/skills/ghostget/references/reddit-flair.md +78 -0
- package/skills/ghostget/references/safety-and-state.md +214 -0
- package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
- package/skills/ghostget/references/social-platform-routing.md +66 -0
- package/skills/ghostget/references/social-profile-stats.md +195 -0
- package/skills/ghostget/references/social-video-platform-routing.md +89 -0
- package/skills/ghostget/references/whatsapp-adapter.md +101 -0
- package/skills/ghostget/references/x-adapter.md +332 -0
- package/skills/ghostget/references/x-ai-disclosure.md +71 -0
- package/src/apple-photos-cli.ts +639 -0
- package/src/apple-photos-client-types.ts +168 -0
- package/src/apple-photos-client.ts +201 -0
- package/src/apple-photos-contact-evidence.ts +838 -0
- package/src/apple-photos-local-source.ts +1131 -0
- package/src/args.ts +2053 -0
- package/src/article-draft-document.ts +417 -0
- package/src/article-draft-embeds.ts +123 -0
- package/src/article-draft-images.ts +214 -0
- package/src/assets/adapter-template/wrench-adapter.json +40 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
- package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
- package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
- package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
- package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
- package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
- package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
- package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
- package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
- package/src/assets/adapters/x/wrench-adapter.json +749 -0
- package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
- package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
- package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
- package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
- package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
- package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
- package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
- package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/assets.d.ts +4 -0
- package/src/auth.ts +1157 -0
- package/src/beeper-client-types.ts +169 -0
- package/src/beeper-client.ts +250 -0
- package/src/beeper-contact-interactions-cli.ts +180 -0
- package/src/beeper-contact-interactions.ts +1612 -0
- package/src/beeper-message-bundle-v1.ts +257 -0
- package/src/beeper-message-like-me-cli.ts +84 -0
- package/src/beeper-message-like-me-export.ts +2274 -0
- package/src/beeper-message-like-me-recovery.ts +931 -0
- package/src/beeper-message-like-me-source.ts +4034 -0
- package/src/browser-admission.ts +928 -0
- package/src/browser-snapshots.ts +260 -0
- package/src/browser.ts +3873 -0
- package/src/canonical-json.ts +60 -0
- package/src/catalog-cli.ts +638 -0
- package/src/cli.ts +208 -0
- package/src/client-types.ts +301 -0
- package/src/client.ts +2726 -0
- package/src/confirmed-write-failure.ts +44 -0
- package/src/confirmed-write-model.ts +72 -0
- package/src/confirmed-write-platform.ts +1055 -0
- package/src/confirmed-write-program.ts +224 -0
- package/src/confirmed-write-runtime.ts +16 -0
- package/src/cursor-token.ts +228 -0
- package/src/derivation-dnr-readiness.ts +367 -0
- package/src/derivation-file-chooser.ts +384 -0
- package/src/derivation-network-boundary.ts +835 -0
- package/src/derivation-network-guard.ts +782 -0
- package/src/derivation-network-proxy-helper.ts +440 -0
- package/src/derivation-network-proxy.ts +385 -0
- package/src/derive-command-helper.ts +693 -0
- package/src/derive-fixtures.ts +343 -0
- package/src/derive-review.ts +980 -0
- package/src/derive.ts +4754 -0
- package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
- package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
- package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
- package/src/ghostget.ts +3672 -0
- package/src/gmail-capture.ts +1244 -0
- package/src/har-internal.ts +1242 -0
- package/src/har.ts +743 -0
- package/src/index.ts +218 -0
- package/src/invocation-read-platform.ts +55 -0
- package/src/invocation-read-program.ts +93 -0
- package/src/invocation-read-runtime.ts +11 -0
- package/src/linked-device-lifecycle-admission.ts +450 -0
- package/src/linked-device-lifecycle-journal.ts +1560 -0
- package/src/linked-device-lifecycle-runtime.ts +1602 -0
- package/src/local-cli-admission.ts +135 -0
- package/src/local-cli-contracts.ts +271 -0
- package/src/local-cli-execution.ts +98 -0
- package/src/local-cli-surface-contract.ts +1510 -0
- package/src/local-cli-tool-identity.ts +336 -0
- package/src/media/archive.ts +2325 -0
- package/src/media/args.ts +303 -0
- package/src/media/cli.ts +287 -0
- package/src/media/doctor.ts +475 -0
- package/src/media/ffmpeg.ts +1330 -0
- package/src/media/http-capture.ts +711 -0
- package/src/media/http-probe.ts +593 -0
- package/src/media/http.ts +387 -0
- package/src/media/index.ts +10 -0
- package/src/media/local-transcription.ts +613 -0
- package/src/media/lock.ts +321 -0
- package/src/media/manifest.ts +1712 -0
- package/src/media/metadata.ts +906 -0
- package/src/media/process.ts +723 -0
- package/src/media/revision.ts +166 -0
- package/src/media/runtime-closure.ts +1063 -0
- package/src/media/source-router.ts +187 -0
- package/src/media/transcriber-config.ts +1122 -0
- package/src/media/transcript-persistence-model.ts +15 -0
- package/src/media/transcript-persistence-platform.ts +89 -0
- package/src/media/transcript-persistence-program.ts +39 -0
- package/src/media/transcript-persistence-runtime.ts +24 -0
- package/src/media/transcript.ts +616 -0
- package/src/media/utf8-order.ts +30 -0
- package/src/media/whisper-cpp.ts +464 -0
- package/src/media/whisper-language.ts +58 -0
- package/src/media/yt-dlp.ts +439 -0
- package/src/message-like-me-agentic-messaging.ts +882 -0
- package/src/messaging-action-store.ts +793 -0
- package/src/messaging-runtime.ts +2136 -0
- package/src/messaging-store.ts +539 -0
- package/src/messaging-types.ts +1663 -0
- package/src/messaging.ts +334 -0
- package/src/model.ts +2671 -0
- package/src/oauth-google.ts +888 -0
- package/src/omni-client-types.ts +306 -0
- package/src/omni-client.ts +2225 -0
- package/src/omni-limits.ts +34 -0
- package/src/omni-model.ts +1614 -0
- package/src/omni-request.ts +379 -0
- package/src/omni-runtime.ts +1632 -0
- package/src/operation-deadline.ts +189 -0
- package/src/path-helper.ts +1656 -0
- package/src/pinned-https.ts +194 -0
- package/src/plan-assets.ts +832 -0
- package/src/platform-catalog.ts +1832 -0
- package/src/plugins/beeper-linked-device/plugin.ts +310 -0
- package/src/plugins/bluesky-web/plugin.ts +178 -0
- package/src/plugins/clasificados-web/plugin.ts +66 -0
- package/src/plugins/github-web/plugin.ts +80 -0
- package/src/plugins/gmail-official/plugin.ts +110 -0
- package/src/plugins/hacker-news-web/plugin.ts +52 -0
- package/src/plugins/imessage-direct/plugin.ts +195 -0
- package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
- package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
- package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
- package/src/plugins/linkedin-official/plugin.ts +84 -0
- package/src/plugins/linkedin-web/plugin.ts +412 -0
- package/src/plugins/meta-web/plugin.ts +317 -0
- package/src/plugins/reddit-web/flair.ts +492 -0
- package/src/plugins/reddit-web/plugin.ts +477 -0
- package/src/plugins/substack-web/plugin.ts +124 -0
- package/src/plugins/tiktok-web/plugin.ts +67 -0
- package/src/plugins/twitch-web/plugin.ts +54 -0
- package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
- package/src/plugins/x-official/plugin.ts +64 -0
- package/src/plugins/x-web/plugin.ts +488 -0
- package/src/plugins/youtube-web/plugin.ts +101 -0
- package/src/portable-run-recovery.ts +466 -0
- package/src/process-identity.ts +501 -0
- package/src/profile-clone-helper.ts +104 -0
- package/src/provider-catalog-views.ts +51 -0
- package/src/provider-context.ts +69 -0
- package/src/provider-contract-definitions-x.ts +45 -0
- package/src/provider-contract-definitions.ts +948 -0
- package/src/provider-contract-input-gmail.ts +176 -0
- package/src/provider-contract-input-linkedin.ts +77 -0
- package/src/provider-contract-input-x.ts +304 -0
- package/src/provider-contract-planning.ts +23 -0
- package/src/provider-contract-semantic-identity.ts +54 -0
- package/src/provider-contracts.ts +251 -0
- package/src/provider-http.ts +723 -0
- package/src/provider-plugin-auth.ts +36 -0
- package/src/provider-plugin-builtins.ts +229 -0
- package/src/provider-plugin-cleanup-barrier.ts +229 -0
- package/src/provider-plugin-cleanup-execution.ts +79 -0
- package/src/provider-plugin-cleanup-resource.ts +470 -0
- package/src/provider-plugin-contract-identity.ts +707 -0
- package/src/provider-plugin-host.ts +1972 -0
- package/src/provider-plugin-identifiers.ts +60 -0
- package/src/provider-plugin-invocation-lease.ts +889 -0
- package/src/provider-plugin-lifecycle-kernel.ts +566 -0
- package/src/provider-plugin-lifecycle.ts +1455 -0
- package/src/provider-plugin-module-analysis.ts +336 -0
- package/src/provider-plugin-package.ts +2530 -0
- package/src/provider-plugin-portable-authority.ts +106 -0
- package/src/provider-plugin-portable-catalog.ts +94 -0
- package/src/provider-plugin-portable-identity.ts +315 -0
- package/src/provider-plugin-portable-runtime.ts +2057 -0
- package/src/provider-plugin-protocol.ts +1625 -0
- package/src/provider-plugin-registry.ts +3342 -0
- package/src/provider-plugin-store.ts +2155 -0
- package/src/provider-plugin-test-fixture.ts +5 -0
- package/src/provider-plugin.ts +4907 -0
- package/src/provider-plugins.generated.ts +43 -0
- package/src/provider-plugins.ts +8 -0
- package/src/provider-subject.ts +41 -0
- package/src/provider.ts +315 -0
- package/src/providers/beeper-local-runtime.ts +5426 -0
- package/src/providers/beeper-local.ts +2739 -0
- package/src/providers/beeper-messaging.ts +462 -0
- package/src/providers/beeper-omni.ts +1159 -0
- package/src/providers/bluesky-web-runtime.ts +2895 -0
- package/src/providers/bluesky-web.ts +1614 -0
- package/src/providers/clasificados-web-runtime.ts +255 -0
- package/src/providers/clasificados-web.ts +503 -0
- package/src/providers/contact-projection.ts +301 -0
- package/src/providers/github-read-model.ts +94 -0
- package/src/providers/github-read-platform.ts +165 -0
- package/src/providers/github-read-program.ts +97 -0
- package/src/providers/github-web-runtime.ts +320 -0
- package/src/providers/github-web.ts +403 -0
- package/src/providers/gmail-api.ts +2834 -0
- package/src/providers/gmail-omni.ts +830 -0
- package/src/providers/gmail.ts +1130 -0
- package/src/providers/hacker-news-web-runtime.ts +220 -0
- package/src/providers/hacker-news-web.ts +809 -0
- package/src/providers/imessage-direct-install.ts +354 -0
- package/src/providers/imessage-direct-messaging.ts +532 -0
- package/src/providers/imessage-direct-omni.ts +400 -0
- package/src/providers/imessage-direct-runtime.ts +1498 -0
- package/src/providers/imessage-direct.ts +351 -0
- package/src/providers/instagram-video-foundations.ts +913 -0
- package/src/providers/instagram-web-profile-browser.ts +600 -0
- package/src/providers/iso-bmff.ts +338 -0
- package/src/providers/linkedin-company-platform.ts +42 -0
- package/src/providers/linkedin-company-program.ts +88 -0
- package/src/providers/linkedin-contact-failure.ts +84 -0
- package/src/providers/linkedin-contact-platform.ts +45 -0
- package/src/providers/linkedin-contact-program.ts +91 -0
- package/src/providers/linkedin-profile-activity-failure.ts +76 -0
- package/src/providers/linkedin-profile-activity-platform.ts +41 -0
- package/src/providers/linkedin-profile-activity-program.ts +67 -0
- package/src/providers/linkedin-read-failure.ts +52 -0
- package/src/providers/linkedin-self-platform.ts +53 -0
- package/src/providers/linkedin-self-program.ts +122 -0
- package/src/providers/linkedin-web-article-browser.ts +794 -0
- package/src/providers/linkedin-web-bootstrap.ts +146 -0
- package/src/providers/linkedin-web-contact.ts +830 -0
- package/src/providers/linkedin-web-feed-browser.ts +832 -0
- package/src/providers/linkedin-web-feed.ts +864 -0
- package/src/providers/linkedin-web-post-browser.ts +653 -0
- package/src/providers/linkedin-web-profile-browser.ts +683 -0
- package/src/providers/linkedin-web-runtime.ts +2388 -0
- package/src/providers/linkedin-web.ts +3305 -0
- package/src/providers/linkedin.ts +1885 -0
- package/src/providers/meta-bootstrap.ts +1043 -0
- package/src/providers/meta-facebook-group.ts +690 -0
- package/src/providers/meta-marketplace-relay.ts +747 -0
- package/src/providers/meta-omni.ts +373 -0
- package/src/providers/meta-relay-bundle.ts +1049 -0
- package/src/providers/meta-web-descriptors.ts +1740 -0
- package/src/providers/meta-web-runtime.ts +3388 -0
- package/src/providers/meta-web.ts +2931 -0
- package/src/providers/read-failure.ts +139 -0
- package/src/providers/reddit-omni.ts +462 -0
- package/src/providers/reddit-web-runtime.ts +1929 -0
- package/src/providers/reddit-web.ts +1611 -0
- package/src/providers/rental-listings.ts +460 -0
- package/src/providers/substack-omni.ts +406 -0
- package/src/providers/substack-video-mp4.ts +33 -0
- package/src/providers/substack-web-runtime.ts +2812 -0
- package/src/providers/substack-web.ts +1502 -0
- package/src/providers/tiktok-video-mp4.ts +33 -0
- package/src/providers/tiktok-web-runtime.ts +770 -0
- package/src/providers/tiktok-web.ts +2786 -0
- package/src/providers/twitch-web-runtime.ts +226 -0
- package/src/providers/twitch-web.ts +274 -0
- package/src/providers/whatsapp-account-identity.ts +55 -0
- package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
- package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
- package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
- package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
- package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
- package/src/providers/whatsapp-messaging.ts +132 -0
- package/src/providers/whatsapp-omni.ts +586 -0
- package/src/providers/whatsapp-web-runtime.ts +3108 -0
- package/src/providers/whatsapp-web.ts +1037 -0
- package/src/providers/x-image-provenance.ts +523 -0
- package/src/providers/x-made-with-ai.ts +106 -0
- package/src/providers/x-transaction-id.ts +413 -0
- package/src/providers/x-web-runtime.ts +3485 -0
- package/src/providers/x-web.ts +2332 -0
- package/src/providers/x.ts +1889 -0
- package/src/providers/youtube-web-runtime.ts +1400 -0
- package/src/providers/youtube-web.ts +1221 -0
- package/src/read-admission-runtime.ts +91 -0
- package/src/read-client.ts +521 -0
- package/src/read-effect-platform.ts +7 -0
- package/src/read-effect-runtime.ts +14 -0
- package/src/read-effect.ts +52 -0
- package/src/read-projection-admission.ts +866 -0
- package/src/read-projections.ts +3151 -0
- package/src/recovery.ts +1492 -0
- package/src/reviewed-template.ts +146 -0
- package/src/run-journal.ts +1272 -0
- package/src/runtime.ts +5116 -0
- package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
- package/src/scripts/install-whatsapp-protocol.sh +232 -0
- package/src/scripts/recover-install-stages.ts +1491 -0
- package/src/scripts/resolve-state-home.ts +30 -0
- package/src/scripts/scaffold-web-provider.ts +973 -0
- package/src/scripts/sync-bundled-adapters.ts +695 -0
- package/src/session-secrets.ts +1183 -0
- package/src/state-helper.bunfig.toml +2 -0
- package/src/state-helper.ts +2914 -0
- package/src/storage.ts +3842 -0
- package/src/transport-policy.ts +2 -0
- package/src/usage.ts +182 -0
- package/src/version.ts +2 -0
- package/src/web-session-authentication-policy.ts +136 -0
- package/src/web-session-cleanup-admission.ts +2344 -0
- package/src/web-session-cleanup-join.ts +99 -0
- package/src/web-session-client.ts +996 -0
- package/src/web-session-contract-definitions.ts +820 -0
- package/src/web-session-contract-planning.ts +42 -0
- package/src/web-session-contracts.ts +228 -0
- package/src/web-session-cookies.ts +500 -0
- package/src/web-session-execution.ts +385 -0
- package/src/web-session-http.ts +373 -0
- package/src/web-session-read-errors.ts +19 -0
- package/src/web-session-read-runtime.ts +124 -0
- package/src/web-session-recovery.ts +818 -0
- package/src/web-session-sites.ts +58 -0
- package/src/web-session-template.ts +994 -0
- package/src/web-session.ts +93 -0
- package/src/whatsapp-client-binding.ts +21 -0
- package/src/whatsapp-client-types.ts +75 -0
- package/src/whatsapp-client.ts +395 -0
- package/src/whatsapp-export-coordinate.ts +17 -0
- package/src/whatsapp-message-bundle-v2.ts +195 -0
- package/src/whatsapp-message-like-me-cli.ts +195 -0
- package/src/whatsapp-message-like-me-export.ts +1072 -0
- package/src/whatsapp-message-like-me-source.ts +1012 -0
- package/tsconfig.json +67 -0
|
@@ -0,0 +1,2344 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { types as nodeTypes } from "node:util";
|
|
4
|
+
|
|
5
|
+
import { canonicalJson, sha256 } from "./canonical-json";
|
|
6
|
+
import {
|
|
7
|
+
AgentBrowserLiveControlUnavailableError,
|
|
8
|
+
adoptLiveLegacyBrowserCleanupResource,
|
|
9
|
+
browserCleanupResourceExtends,
|
|
10
|
+
browserCleanupResourceRootStatus,
|
|
11
|
+
bindLiveAgentBrowserCleanupResource,
|
|
12
|
+
parseBrowserCleanupResourceIdentity,
|
|
13
|
+
PreservedBrowserArtifactsError,
|
|
14
|
+
provePreparedAgentBrowserCleanupResourceQuiescent,
|
|
15
|
+
recoverPinnedAgentBrowserCleanupResource,
|
|
16
|
+
refreshBrowserCleanupResourceQuiescence,
|
|
17
|
+
reproveBrowserCleanupAfterArtifactsRemoval,
|
|
18
|
+
type AgentBrowserLifecycleDependencies,
|
|
19
|
+
type BrowserCleanupResourceIdentity,
|
|
20
|
+
type BrowserCleanupResourceIdentityV2,
|
|
21
|
+
} from "./browser";
|
|
22
|
+
import {
|
|
23
|
+
currentProcessStartIdentity,
|
|
24
|
+
processOwnerStatus,
|
|
25
|
+
type ProcessOwnerIdentity,
|
|
26
|
+
type ProcessOwnerStatus,
|
|
27
|
+
} from "./process-identity";
|
|
28
|
+
import {
|
|
29
|
+
localCliCleanupProcessGroupStatus,
|
|
30
|
+
localCliCleanupResourceExtends,
|
|
31
|
+
parseLocalCliCleanupResourceIdentityV1,
|
|
32
|
+
type ProviderPluginCleanupResourceIdentity,
|
|
33
|
+
} from "./provider-plugin-cleanup-resource";
|
|
34
|
+
import {
|
|
35
|
+
createPrivateJsonIfAbsent,
|
|
36
|
+
ensurePrivateStateDirectory,
|
|
37
|
+
ghostgetStateHome,
|
|
38
|
+
readPrivateStateFileIfPresent,
|
|
39
|
+
readPrivateStateFilesBatched,
|
|
40
|
+
removePrivateDirectoryTree,
|
|
41
|
+
removePrivateStateFileIfUnchanged,
|
|
42
|
+
snapshotPrivateStateDirectory,
|
|
43
|
+
writePrivateJsonIfUnchanged,
|
|
44
|
+
} from "./storage";
|
|
45
|
+
import type {
|
|
46
|
+
WebSessionCleanupBarrierRegistrar,
|
|
47
|
+
} from "./web-session-execution";
|
|
48
|
+
|
|
49
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
50
|
+
type JsonRecord = Record<string, unknown>;
|
|
51
|
+
|
|
52
|
+
const LEGACY_ADMISSION_SCHEMA_VERSION = 1 as const;
|
|
53
|
+
const ADMISSION_SCHEMA_VERSION = 2 as const;
|
|
54
|
+
const MAX_ADMISSION_BYTES = 64 * 1024;
|
|
55
|
+
const MAX_ACQUISITION_ATTEMPTS = 8;
|
|
56
|
+
const digestPattern = /^[a-f0-9]{64}$/u;
|
|
57
|
+
const idPattern = /^[a-z][a-z0-9-]{0,127}$/u;
|
|
58
|
+
const authIdPattern = /^[a-z][a-z0-9-]{0,47}$/u;
|
|
59
|
+
const uuidPattern =
|
|
60
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
61
|
+
const claimNamePattern = /^([a-f0-9]{64})\.json$/u;
|
|
62
|
+
const storageHelperArtifactPatterns = Object.freeze([
|
|
63
|
+
/^\.io-write-[1-9][0-9]{0,9}-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/u,
|
|
64
|
+
/^\.io-mutation-[a-f0-9]{64}-(?:waiting|candidate|held)-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.lock$/u,
|
|
65
|
+
/^\.io-mutation-stage-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}-[1-9][0-9]{0,9}\.tmp$/u,
|
|
66
|
+
/^\.io-remove-file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.quarantine$/u,
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
export const WEB_SESSION_CLEANUP_ADMISSION_STATE_DIRECTORY =
|
|
70
|
+
"provider-plugin-state/.web-session-cleanup-admissions" as const;
|
|
71
|
+
|
|
72
|
+
export type WebSessionCleanupAdmissionIdentity = {
|
|
73
|
+
readonly runId: string;
|
|
74
|
+
readonly pluginId: string;
|
|
75
|
+
readonly pluginVersion: string;
|
|
76
|
+
readonly pluginImplementationHash: string;
|
|
77
|
+
readonly adapterId: string;
|
|
78
|
+
readonly adapterHash: string;
|
|
79
|
+
readonly surfaceId: string;
|
|
80
|
+
readonly authId: string;
|
|
81
|
+
readonly authHash: string;
|
|
82
|
+
/** Present for new claims; omitted only by bounded predecessor readers. */
|
|
83
|
+
readonly transport?: "web-session-api" | "local-cli";
|
|
84
|
+
/** Exact plugin/tool/artifact execution identity for this admitted run. */
|
|
85
|
+
readonly executionIdentityHash?: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type WebSessionCleanupAdmissionContainment =
|
|
89
|
+
| { readonly status: "parent-owned" }
|
|
90
|
+
| {
|
|
91
|
+
readonly status:
|
|
92
|
+
| "resource-active"
|
|
93
|
+
| "cleanup-complete"
|
|
94
|
+
| "cleanup-unsafe";
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type WebSessionCleanupAdmissionResource =
|
|
98
|
+
| {
|
|
99
|
+
readonly resourceId: string;
|
|
100
|
+
readonly status: "unpublished";
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
readonly resourceId: string;
|
|
104
|
+
readonly status: "active";
|
|
105
|
+
readonly identity: ProviderPluginCleanupResourceIdentity;
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
readonly resourceId: string;
|
|
109
|
+
readonly status: "browser-closed-artifacts";
|
|
110
|
+
readonly identity: BrowserCleanupResourceIdentity;
|
|
111
|
+
}
|
|
112
|
+
| {
|
|
113
|
+
readonly resourceId: string;
|
|
114
|
+
/**
|
|
115
|
+
* The pinned browser lifecycle has reached a durable quiescent boundary.
|
|
116
|
+
* Root removal progress is journaled so a crash after either exact
|
|
117
|
+
* deletion can resume without weakening the lifecycle proof.
|
|
118
|
+
*/
|
|
119
|
+
readonly status: "browser-quiescent-artifacts";
|
|
120
|
+
readonly identity: BrowserCleanupResourceIdentityV2;
|
|
121
|
+
readonly removedRoots: readonly BrowserCleanupRootName[];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type BrowserCleanupRootName = "artifacts" | "socket";
|
|
125
|
+
|
|
126
|
+
type CleanupAdmissionOwner = ProcessOwnerIdentity & {
|
|
127
|
+
readonly token: string;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type WebSessionCleanupAdmissionRecovery =
|
|
131
|
+
| { readonly status: "idle" }
|
|
132
|
+
| {
|
|
133
|
+
readonly status: "active";
|
|
134
|
+
readonly owner: CleanupAdmissionOwner;
|
|
135
|
+
readonly acquiredAt: string;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type WebSessionCleanupAdmissionClaimFields = {
|
|
139
|
+
readonly realmKey: string;
|
|
140
|
+
readonly runId: string;
|
|
141
|
+
readonly pluginId: string;
|
|
142
|
+
readonly pluginVersion: string;
|
|
143
|
+
readonly pluginImplementationHash: string;
|
|
144
|
+
readonly adapterId: string;
|
|
145
|
+
readonly adapterHash: string;
|
|
146
|
+
readonly surfaceId: string;
|
|
147
|
+
readonly authId: string;
|
|
148
|
+
readonly authHash: string;
|
|
149
|
+
readonly transport?: "web-session-api" | "local-cli";
|
|
150
|
+
readonly executionIdentityHash?: string;
|
|
151
|
+
readonly owner: CleanupAdmissionOwner;
|
|
152
|
+
readonly acquiredAt: string;
|
|
153
|
+
readonly containment: WebSessionCleanupAdmissionContainment;
|
|
154
|
+
readonly resources: readonly WebSessionCleanupAdmissionResource[];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type WebSessionCleanupAdmissionClaim =
|
|
158
|
+
| (WebSessionCleanupAdmissionClaimFields & {
|
|
159
|
+
readonly schemaVersion: typeof LEGACY_ADMISSION_SCHEMA_VERSION;
|
|
160
|
+
})
|
|
161
|
+
| (WebSessionCleanupAdmissionClaimFields & {
|
|
162
|
+
readonly schemaVersion: typeof ADMISSION_SCHEMA_VERSION;
|
|
163
|
+
readonly recovery: WebSessionCleanupAdmissionRecovery;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export type WebSessionCleanupAdmissionSnapshot = {
|
|
167
|
+
readonly claim: WebSessionCleanupAdmissionClaim;
|
|
168
|
+
readonly contentSha256: string;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export type WebSessionCleanupAdmissionListEntry =
|
|
172
|
+
| WebSessionCleanupAdmissionSnapshot
|
|
173
|
+
| {
|
|
174
|
+
readonly coordinate: string;
|
|
175
|
+
readonly invalid: true;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export type WebSessionCleanupAdmissionRecoveryIssue = {
|
|
179
|
+
readonly coordinate: string;
|
|
180
|
+
readonly kind:
|
|
181
|
+
| "invalid-admission"
|
|
182
|
+
| "owner-unknown"
|
|
183
|
+
| "resource-active"
|
|
184
|
+
| "cleanup-unsafe"
|
|
185
|
+
| "recovery-active"
|
|
186
|
+
| "recovery-conflict";
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export type WebSessionCleanupAdmissionRecoveryReport = {
|
|
190
|
+
readonly scanned: number;
|
|
191
|
+
readonly repaired: number;
|
|
192
|
+
readonly active: number;
|
|
193
|
+
readonly retained: number;
|
|
194
|
+
readonly invalid: number;
|
|
195
|
+
readonly issues: readonly WebSessionCleanupAdmissionRecoveryIssue[];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
function record(value: unknown, label: string): JsonRecord {
|
|
199
|
+
if (
|
|
200
|
+
typeof value !== "object"
|
|
201
|
+
|| value === null
|
|
202
|
+
|| Array.isArray(value)
|
|
203
|
+
|| nodeTypes.isProxy(value)
|
|
204
|
+
) {
|
|
205
|
+
throw new Error(`${label} must be an object`);
|
|
206
|
+
}
|
|
207
|
+
const prototype: unknown = Object.getPrototypeOf(value) as unknown;
|
|
208
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
209
|
+
throw new Error(`${label} must be a plain object`);
|
|
210
|
+
}
|
|
211
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
212
|
+
const result: JsonRecord = {};
|
|
213
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
214
|
+
if (typeof key !== "string") {
|
|
215
|
+
throw new Error(`${label} has unsupported symbol fields`);
|
|
216
|
+
}
|
|
217
|
+
const descriptor = descriptors[key];
|
|
218
|
+
if (
|
|
219
|
+
descriptor === undefined
|
|
220
|
+
|| !descriptor.enumerable
|
|
221
|
+
|| !("value" in descriptor)
|
|
222
|
+
) {
|
|
223
|
+
throw new Error(`${label} has unsupported accessor fields`);
|
|
224
|
+
}
|
|
225
|
+
result[key] = descriptor.value as unknown;
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function exactKeys(
|
|
231
|
+
value: JsonRecord,
|
|
232
|
+
expectedKeys: readonly string[],
|
|
233
|
+
label: string,
|
|
234
|
+
): void {
|
|
235
|
+
const actual = Object.keys(value).sort();
|
|
236
|
+
const expected = [...expectedKeys].sort();
|
|
237
|
+
if (
|
|
238
|
+
actual.length !== expected.length
|
|
239
|
+
|| actual.some((key, index) => key !== expected[index])
|
|
240
|
+
) {
|
|
241
|
+
throw new Error(`${label} has unsupported fields`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function digest(value: unknown, label: string): string {
|
|
246
|
+
if (typeof value !== "string" || !digestPattern.test(value)) {
|
|
247
|
+
throw new Error(`${label} is malformed`);
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function identifier(
|
|
253
|
+
value: unknown,
|
|
254
|
+
label: string,
|
|
255
|
+
pattern = idPattern,
|
|
256
|
+
): string {
|
|
257
|
+
if (typeof value !== "string" || !pattern.test(value)) {
|
|
258
|
+
throw new Error(`${label} is malformed`);
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function uuid(value: unknown, label: string): string {
|
|
264
|
+
if (typeof value !== "string" || !uuidPattern.test(value)) {
|
|
265
|
+
throw new Error(`${label} is malformed`);
|
|
266
|
+
}
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function timestamp(value: unknown, label: string): string {
|
|
271
|
+
if (
|
|
272
|
+
typeof value !== "string"
|
|
273
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
|
|
274
|
+
|| new Date(value).toISOString() !== value
|
|
275
|
+
) {
|
|
276
|
+
throw new Error(`${label} is malformed`);
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function parseOwner(
|
|
282
|
+
value: unknown,
|
|
283
|
+
): WebSessionCleanupAdmissionClaim["owner"] {
|
|
284
|
+
const owner = record(value, "web-session cleanup admission owner");
|
|
285
|
+
exactKeys(
|
|
286
|
+
owner,
|
|
287
|
+
["pid", "token", "bootId", "processStartId"],
|
|
288
|
+
"web-session cleanup admission owner",
|
|
289
|
+
);
|
|
290
|
+
if (
|
|
291
|
+
typeof owner.pid !== "number"
|
|
292
|
+
|| !Number.isSafeInteger(owner.pid)
|
|
293
|
+
|| owner.pid < 1
|
|
294
|
+
) {
|
|
295
|
+
throw new Error("web-session cleanup admission owner PID is malformed");
|
|
296
|
+
}
|
|
297
|
+
return Object.freeze({
|
|
298
|
+
pid: owner.pid,
|
|
299
|
+
token: uuid(
|
|
300
|
+
owner.token,
|
|
301
|
+
"web-session cleanup admission owner token",
|
|
302
|
+
),
|
|
303
|
+
bootId: digest(
|
|
304
|
+
owner.bootId,
|
|
305
|
+
"web-session cleanup admission owner boot identity",
|
|
306
|
+
),
|
|
307
|
+
processStartId: digest(
|
|
308
|
+
owner.processStartId,
|
|
309
|
+
"web-session cleanup admission owner process identity",
|
|
310
|
+
),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function parseRecovery(
|
|
315
|
+
value: unknown,
|
|
316
|
+
): WebSessionCleanupAdmissionRecovery {
|
|
317
|
+
const recovery = record(
|
|
318
|
+
value,
|
|
319
|
+
"web-session cleanup admission recovery",
|
|
320
|
+
);
|
|
321
|
+
if (recovery.status === "idle") {
|
|
322
|
+
exactKeys(
|
|
323
|
+
recovery,
|
|
324
|
+
["status"],
|
|
325
|
+
"web-session cleanup admission recovery",
|
|
326
|
+
);
|
|
327
|
+
return Object.freeze({ status: "idle" });
|
|
328
|
+
}
|
|
329
|
+
exactKeys(
|
|
330
|
+
recovery,
|
|
331
|
+
["status", "owner", "acquiredAt"],
|
|
332
|
+
"web-session cleanup admission recovery",
|
|
333
|
+
);
|
|
334
|
+
if (recovery.status !== "active") {
|
|
335
|
+
throw new Error("web-session cleanup admission recovery is malformed");
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze({
|
|
338
|
+
status: "active",
|
|
339
|
+
owner: parseOwner(recovery.owner),
|
|
340
|
+
acquiredAt: timestamp(
|
|
341
|
+
recovery.acquiredAt,
|
|
342
|
+
"web-session cleanup admission recovery acquisition time",
|
|
343
|
+
),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function parseContainment(
|
|
348
|
+
value: unknown,
|
|
349
|
+
): WebSessionCleanupAdmissionContainment {
|
|
350
|
+
const containment = record(
|
|
351
|
+
value,
|
|
352
|
+
"web-session cleanup admission containment",
|
|
353
|
+
);
|
|
354
|
+
exactKeys(
|
|
355
|
+
containment,
|
|
356
|
+
["status"],
|
|
357
|
+
"web-session cleanup admission containment",
|
|
358
|
+
);
|
|
359
|
+
if (
|
|
360
|
+
containment.status !== "parent-owned"
|
|
361
|
+
&& containment.status !== "resource-active"
|
|
362
|
+
&& containment.status !== "cleanup-complete"
|
|
363
|
+
&& containment.status !== "cleanup-unsafe"
|
|
364
|
+
) {
|
|
365
|
+
throw new Error("web-session cleanup admission containment is malformed");
|
|
366
|
+
}
|
|
367
|
+
return Object.freeze({ status: containment.status });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function parseCleanupResourceIdentity(
|
|
371
|
+
value: unknown,
|
|
372
|
+
): ProviderPluginCleanupResourceIdentity {
|
|
373
|
+
const kind = typeof value === "object"
|
|
374
|
+
&& value !== null
|
|
375
|
+
&& !Array.isArray(value)
|
|
376
|
+
? Object.getOwnPropertyDescriptor(value, "kind")
|
|
377
|
+
: undefined;
|
|
378
|
+
if (
|
|
379
|
+
kind === undefined
|
|
380
|
+
|| !kind.enumerable
|
|
381
|
+
|| !("value" in kind)
|
|
382
|
+
) {
|
|
383
|
+
throw new Error("provider cleanup resource identity kind is malformed");
|
|
384
|
+
}
|
|
385
|
+
return kind.value === "local-cli-private-root-v1"
|
|
386
|
+
? parseLocalCliCleanupResourceIdentityV1(value)
|
|
387
|
+
: parseBrowserCleanupResourceIdentity(value);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function parseCleanupResources(
|
|
391
|
+
value: unknown,
|
|
392
|
+
): readonly WebSessionCleanupAdmissionResource[] {
|
|
393
|
+
if (!Array.isArray(value) || value.length > 8) {
|
|
394
|
+
throw new Error("web-session cleanup resource collection is malformed");
|
|
395
|
+
}
|
|
396
|
+
const resources: WebSessionCleanupAdmissionResource[] = [];
|
|
397
|
+
const resourceIds = new Set<string>();
|
|
398
|
+
for (const valueEntry of value as readonly unknown[]) {
|
|
399
|
+
const entry = record(valueEntry, "web-session cleanup resource");
|
|
400
|
+
if (entry.status === "unpublished") {
|
|
401
|
+
exactKeys(
|
|
402
|
+
entry,
|
|
403
|
+
["resourceId", "status"],
|
|
404
|
+
"web-session cleanup resource",
|
|
405
|
+
);
|
|
406
|
+
const resourceId = uuid(
|
|
407
|
+
entry.resourceId,
|
|
408
|
+
"web-session cleanup resource ID",
|
|
409
|
+
);
|
|
410
|
+
if (resourceIds.has(resourceId)) {
|
|
411
|
+
throw new Error("web-session cleanup resource ID is duplicated");
|
|
412
|
+
}
|
|
413
|
+
resourceIds.add(resourceId);
|
|
414
|
+
resources.push(Object.freeze({
|
|
415
|
+
resourceId,
|
|
416
|
+
status: "unpublished",
|
|
417
|
+
}));
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (
|
|
421
|
+
entry.status !== "active"
|
|
422
|
+
&& entry.status !== "browser-closed-artifacts"
|
|
423
|
+
&& entry.status !== "browser-quiescent-artifacts"
|
|
424
|
+
) {
|
|
425
|
+
throw new Error("web-session cleanup resource status is malformed");
|
|
426
|
+
}
|
|
427
|
+
if (entry.status === "browser-quiescent-artifacts") {
|
|
428
|
+
exactKeys(
|
|
429
|
+
entry,
|
|
430
|
+
["resourceId", "status", "identity", "removedRoots"],
|
|
431
|
+
"web-session cleanup resource",
|
|
432
|
+
);
|
|
433
|
+
const resourceId = uuid(
|
|
434
|
+
entry.resourceId,
|
|
435
|
+
"web-session cleanup resource ID",
|
|
436
|
+
);
|
|
437
|
+
if (resourceIds.has(resourceId)) {
|
|
438
|
+
throw new Error("web-session cleanup resource ID is duplicated");
|
|
439
|
+
}
|
|
440
|
+
resourceIds.add(resourceId);
|
|
441
|
+
const identity = parseBrowserCleanupResourceIdentity(entry.identity);
|
|
442
|
+
if (
|
|
443
|
+
identity.kind !== "agent-browser-session-v2"
|
|
444
|
+
|| !Array.isArray(entry.removedRoots)
|
|
445
|
+
|| entry.removedRoots.length > 2
|
|
446
|
+
|| entry.removedRoots.some((root, index) =>
|
|
447
|
+
root !== (["artifacts", "socket"] as const)[index]
|
|
448
|
+
)
|
|
449
|
+
) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
"web-session cleanup browser root-removal journal is malformed",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
resources.push(Object.freeze({
|
|
455
|
+
resourceId,
|
|
456
|
+
status: "browser-quiescent-artifacts" as const,
|
|
457
|
+
identity,
|
|
458
|
+
removedRoots: Object.freeze(
|
|
459
|
+
[...entry.removedRoots] as BrowserCleanupRootName[],
|
|
460
|
+
),
|
|
461
|
+
}));
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
exactKeys(
|
|
465
|
+
entry,
|
|
466
|
+
["resourceId", "status", "identity"],
|
|
467
|
+
"web-session cleanup resource",
|
|
468
|
+
);
|
|
469
|
+
const resourceId = uuid(
|
|
470
|
+
entry.resourceId,
|
|
471
|
+
"web-session cleanup resource ID",
|
|
472
|
+
);
|
|
473
|
+
if (resourceIds.has(resourceId)) {
|
|
474
|
+
throw new Error("web-session cleanup resource ID is duplicated");
|
|
475
|
+
}
|
|
476
|
+
resourceIds.add(resourceId);
|
|
477
|
+
resources.push(entry.status === "browser-closed-artifacts"
|
|
478
|
+
? Object.freeze({
|
|
479
|
+
resourceId,
|
|
480
|
+
status: "browser-closed-artifacts" as const,
|
|
481
|
+
identity: parseBrowserCleanupResourceIdentity(entry.identity),
|
|
482
|
+
})
|
|
483
|
+
: Object.freeze({
|
|
484
|
+
resourceId,
|
|
485
|
+
status: "active" as const,
|
|
486
|
+
identity: parseCleanupResourceIdentity(entry.identity),
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
return Object.freeze(resources);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function webSessionCleanupRealmKey(
|
|
493
|
+
surfaceIdValue: string,
|
|
494
|
+
authIdValue: string,
|
|
495
|
+
): string {
|
|
496
|
+
const surfaceId = identifier(
|
|
497
|
+
surfaceIdValue,
|
|
498
|
+
"web-session cleanup surface ID",
|
|
499
|
+
);
|
|
500
|
+
const authId = identifier(
|
|
501
|
+
authIdValue,
|
|
502
|
+
"web-session cleanup auth ID",
|
|
503
|
+
authIdPattern,
|
|
504
|
+
);
|
|
505
|
+
return sha256(
|
|
506
|
+
`io-web-session-cleanup-realm-v1\0${canonicalJson({
|
|
507
|
+
surfaceId,
|
|
508
|
+
authId,
|
|
509
|
+
})}`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function parseWebSessionCleanupAdmissionClaim(
|
|
514
|
+
value: unknown,
|
|
515
|
+
): WebSessionCleanupAdmissionClaim {
|
|
516
|
+
const claim = record(value, "web-session cleanup admission");
|
|
517
|
+
if (
|
|
518
|
+
claim.schemaVersion !== LEGACY_ADMISSION_SCHEMA_VERSION
|
|
519
|
+
&& claim.schemaVersion !== ADMISSION_SCHEMA_VERSION
|
|
520
|
+
) {
|
|
521
|
+
throw new Error("web-session cleanup admission version is unsupported");
|
|
522
|
+
}
|
|
523
|
+
exactKeys(
|
|
524
|
+
claim,
|
|
525
|
+
[
|
|
526
|
+
"schemaVersion",
|
|
527
|
+
"realmKey",
|
|
528
|
+
"runId",
|
|
529
|
+
"pluginId",
|
|
530
|
+
"pluginVersion",
|
|
531
|
+
"pluginImplementationHash",
|
|
532
|
+
"adapterId",
|
|
533
|
+
"adapterHash",
|
|
534
|
+
"surfaceId",
|
|
535
|
+
"authId",
|
|
536
|
+
"authHash",
|
|
537
|
+
"owner",
|
|
538
|
+
"acquiredAt",
|
|
539
|
+
"containment",
|
|
540
|
+
"resources",
|
|
541
|
+
...(claim.schemaVersion === ADMISSION_SCHEMA_VERSION
|
|
542
|
+
? ["recovery"]
|
|
543
|
+
: []),
|
|
544
|
+
...(claim.transport === undefined ? [] : ["transport"]),
|
|
545
|
+
...(claim.executionIdentityHash === undefined
|
|
546
|
+
? []
|
|
547
|
+
: ["executionIdentityHash"]),
|
|
548
|
+
],
|
|
549
|
+
"web-session cleanup admission",
|
|
550
|
+
);
|
|
551
|
+
if (
|
|
552
|
+
(claim.transport === undefined)
|
|
553
|
+
!== (claim.executionIdentityHash === undefined)
|
|
554
|
+
|| (
|
|
555
|
+
claim.transport !== undefined
|
|
556
|
+
&& claim.transport !== "web-session-api"
|
|
557
|
+
&& claim.transport !== "local-cli"
|
|
558
|
+
)
|
|
559
|
+
) {
|
|
560
|
+
throw new Error("web-session cleanup execution identity is malformed");
|
|
561
|
+
}
|
|
562
|
+
const pluginVersion = claim.pluginVersion;
|
|
563
|
+
if (
|
|
564
|
+
typeof pluginVersion !== "string"
|
|
565
|
+
|| pluginVersion.length < 1
|
|
566
|
+
|| pluginVersion.length > 128
|
|
567
|
+
|| /[\0\r\n]/u.test(pluginVersion)
|
|
568
|
+
) {
|
|
569
|
+
throw new Error("web-session cleanup plugin version is malformed");
|
|
570
|
+
}
|
|
571
|
+
const parsedFields: WebSessionCleanupAdmissionClaimFields = Object.freeze({
|
|
572
|
+
realmKey: digest(
|
|
573
|
+
claim.realmKey,
|
|
574
|
+
"web-session cleanup admission realm key",
|
|
575
|
+
),
|
|
576
|
+
runId: uuid(claim.runId, "web-session cleanup admission run ID"),
|
|
577
|
+
pluginId: identifier(
|
|
578
|
+
claim.pluginId,
|
|
579
|
+
"web-session cleanup admission plugin ID",
|
|
580
|
+
),
|
|
581
|
+
pluginVersion,
|
|
582
|
+
pluginImplementationHash: digest(
|
|
583
|
+
claim.pluginImplementationHash,
|
|
584
|
+
"web-session cleanup admission implementation hash",
|
|
585
|
+
),
|
|
586
|
+
adapterId: identifier(
|
|
587
|
+
claim.adapterId,
|
|
588
|
+
"web-session cleanup admission adapter ID",
|
|
589
|
+
authIdPattern,
|
|
590
|
+
),
|
|
591
|
+
adapterHash: digest(
|
|
592
|
+
claim.adapterHash,
|
|
593
|
+
"web-session cleanup admission adapter hash",
|
|
594
|
+
),
|
|
595
|
+
surfaceId: identifier(
|
|
596
|
+
claim.surfaceId,
|
|
597
|
+
"web-session cleanup admission surface ID",
|
|
598
|
+
),
|
|
599
|
+
authId: identifier(
|
|
600
|
+
claim.authId,
|
|
601
|
+
"web-session cleanup admission auth ID",
|
|
602
|
+
authIdPattern,
|
|
603
|
+
),
|
|
604
|
+
authHash: digest(
|
|
605
|
+
claim.authHash,
|
|
606
|
+
"web-session cleanup admission auth hash",
|
|
607
|
+
),
|
|
608
|
+
...(claim.transport === undefined
|
|
609
|
+
? {}
|
|
610
|
+
: {
|
|
611
|
+
transport: claim.transport,
|
|
612
|
+
executionIdentityHash: digest(
|
|
613
|
+
claim.executionIdentityHash,
|
|
614
|
+
"web-session cleanup admission execution identity hash",
|
|
615
|
+
),
|
|
616
|
+
}),
|
|
617
|
+
owner: parseOwner(claim.owner),
|
|
618
|
+
acquiredAt: timestamp(
|
|
619
|
+
claim.acquiredAt,
|
|
620
|
+
"web-session cleanup admission acquisition time",
|
|
621
|
+
),
|
|
622
|
+
containment: parseContainment(claim.containment),
|
|
623
|
+
resources: parseCleanupResources(claim.resources),
|
|
624
|
+
});
|
|
625
|
+
const parsed: WebSessionCleanupAdmissionClaim = claim.schemaVersion
|
|
626
|
+
=== LEGACY_ADMISSION_SCHEMA_VERSION
|
|
627
|
+
? Object.freeze({
|
|
628
|
+
schemaVersion: LEGACY_ADMISSION_SCHEMA_VERSION,
|
|
629
|
+
...parsedFields,
|
|
630
|
+
})
|
|
631
|
+
: Object.freeze({
|
|
632
|
+
schemaVersion: ADMISSION_SCHEMA_VERSION,
|
|
633
|
+
...parsedFields,
|
|
634
|
+
recovery: parseRecovery(claim.recovery),
|
|
635
|
+
});
|
|
636
|
+
if (
|
|
637
|
+
parsed.schemaVersion === LEGACY_ADMISSION_SCHEMA_VERSION
|
|
638
|
+
&& parsed.resources.some((resource) =>
|
|
639
|
+
resource.status === "browser-quiescent-artifacts"
|
|
640
|
+
|| (
|
|
641
|
+
resource.status !== "unpublished"
|
|
642
|
+
&& resource.identity.kind === "agent-browser-session-v2"
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
"legacy web-session cleanup admission contains a future resource state",
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (
|
|
651
|
+
parsed.realmKey
|
|
652
|
+
!== webSessionCleanupRealmKey(parsed.surfaceId, parsed.authId)
|
|
653
|
+
) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
"web-session cleanup admission realm does not match its surface and auth",
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
for (const resource of parsed.resources) {
|
|
659
|
+
if (
|
|
660
|
+
resource.status !== "unpublished"
|
|
661
|
+
&& resource.identity.kind !== "local-cli-private-root-v1"
|
|
662
|
+
&& !resource.identity.session.startsWith(
|
|
663
|
+
`io-${parsed.owner.pid}-`,
|
|
664
|
+
)
|
|
665
|
+
) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
"web-session cleanup browser resource does not match its owning process",
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (
|
|
671
|
+
resource.status !== "unpublished"
|
|
672
|
+
&& (
|
|
673
|
+
(parsed.transport === "local-cli")
|
|
674
|
+
!== (resource.identity.kind === "local-cli-private-root-v1")
|
|
675
|
+
)
|
|
676
|
+
) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
"provider cleanup resource does not match its admitted transport",
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (
|
|
683
|
+
parsed.containment.status === "parent-owned"
|
|
684
|
+
&& parsed.resources.length !== 0
|
|
685
|
+
) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
"parent-owned web-session cleanup admission cannot own resources",
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
if (
|
|
691
|
+
(
|
|
692
|
+
parsed.containment.status === "resource-active"
|
|
693
|
+
|| parsed.containment.status === "cleanup-unsafe"
|
|
694
|
+
)
|
|
695
|
+
&& parsed.resources.length === 0
|
|
696
|
+
) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
"resource-owning web-session cleanup admission omitted its resources",
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
return parsed;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function directory(environment: Environment): string {
|
|
705
|
+
return join(
|
|
706
|
+
ghostgetStateHome(environment),
|
|
707
|
+
WEB_SESSION_CLEANUP_ADMISSION_STATE_DIRECTORY,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function pathFor(realmKeyValue: string, environment: Environment): string {
|
|
712
|
+
return join(
|
|
713
|
+
directory(environment),
|
|
714
|
+
`${digest(
|
|
715
|
+
realmKeyValue,
|
|
716
|
+
"web-session cleanup admission realm key",
|
|
717
|
+
)}.json`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function claimSnapshot(
|
|
722
|
+
claimValue: WebSessionCleanupAdmissionClaim,
|
|
723
|
+
): WebSessionCleanupAdmissionSnapshot {
|
|
724
|
+
const claim = parseWebSessionCleanupAdmissionClaim(claimValue);
|
|
725
|
+
const content = `${canonicalJson(claim)}\n`;
|
|
726
|
+
if (Buffer.byteLength(content, "utf8") > MAX_ADMISSION_BYTES) {
|
|
727
|
+
throw new Error("web-session cleanup admission exceeds its byte bound");
|
|
728
|
+
}
|
|
729
|
+
return Object.freeze({
|
|
730
|
+
claim,
|
|
731
|
+
contentSha256: sha256(content),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function isStorageHelperArtifact(
|
|
736
|
+
entry: {
|
|
737
|
+
readonly name: string;
|
|
738
|
+
readonly kind: "file" | "directory" | "symbolic-link" | "other";
|
|
739
|
+
},
|
|
740
|
+
): boolean {
|
|
741
|
+
return entry.kind === "file"
|
|
742
|
+
&& storageHelperArtifactPatterns.some((pattern) =>
|
|
743
|
+
pattern.test(entry.name)
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export function listWebSessionCleanupAdmissions(
|
|
748
|
+
environment: Environment = process.env,
|
|
749
|
+
): readonly WebSessionCleanupAdmissionListEntry[] {
|
|
750
|
+
const admissionDirectory = directory(environment);
|
|
751
|
+
const directorySnapshot = snapshotPrivateStateDirectory(
|
|
752
|
+
admissionDirectory,
|
|
753
|
+
environment,
|
|
754
|
+
);
|
|
755
|
+
if (directorySnapshot.identity === null) return Object.freeze([]);
|
|
756
|
+
const admissionEntries = directorySnapshot.entries.filter((entry) =>
|
|
757
|
+
!isStorageHelperArtifact(entry)
|
|
758
|
+
);
|
|
759
|
+
const claimCandidates = admissionEntries.filter((entry) =>
|
|
760
|
+
entry.kind === "file" && claimNamePattern.test(entry.name)
|
|
761
|
+
);
|
|
762
|
+
const files = readPrivateStateFilesBatched(
|
|
763
|
+
admissionDirectory,
|
|
764
|
+
claimCandidates.map((entry) => entry.name),
|
|
765
|
+
{
|
|
766
|
+
maximumBytesPerFile: MAX_ADMISSION_BYTES,
|
|
767
|
+
environment,
|
|
768
|
+
expectedDirectoryIdentity: directorySnapshot.identity,
|
|
769
|
+
},
|
|
770
|
+
);
|
|
771
|
+
const byName = new Map(files.map((file) => [file.name, file] as const));
|
|
772
|
+
return Object.freeze(admissionEntries.map((entry) => {
|
|
773
|
+
const match = claimNamePattern.exec(entry.name);
|
|
774
|
+
const coordinate = match?.[1] ?? sha256(entry.name);
|
|
775
|
+
if (
|
|
776
|
+
entry.kind !== "file"
|
|
777
|
+
|| match === null
|
|
778
|
+
|| match[1] === undefined
|
|
779
|
+
) {
|
|
780
|
+
return { coordinate, invalid: true as const };
|
|
781
|
+
}
|
|
782
|
+
const file = byName.get(entry.name);
|
|
783
|
+
if (file?.status !== "present") {
|
|
784
|
+
return { coordinate, invalid: true as const };
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
const claim = parseWebSessionCleanupAdmissionClaim(
|
|
788
|
+
JSON.parse(file.content) as unknown,
|
|
789
|
+
);
|
|
790
|
+
if (
|
|
791
|
+
claim.realmKey !== match[1]
|
|
792
|
+
|| file.content !== `${canonicalJson(claim)}\n`
|
|
793
|
+
) {
|
|
794
|
+
return { coordinate, invalid: true as const };
|
|
795
|
+
}
|
|
796
|
+
return Object.freeze({
|
|
797
|
+
claim,
|
|
798
|
+
contentSha256: sha256(file.content),
|
|
799
|
+
});
|
|
800
|
+
} catch {
|
|
801
|
+
return { coordinate, invalid: true as const };
|
|
802
|
+
}
|
|
803
|
+
}));
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function readWebSessionCleanupAdmission(
|
|
807
|
+
realmKeyValue: string,
|
|
808
|
+
environment: Environment,
|
|
809
|
+
): WebSessionCleanupAdmissionSnapshot | null {
|
|
810
|
+
const realmKey = digest(
|
|
811
|
+
realmKeyValue,
|
|
812
|
+
"web-session cleanup admission realm key",
|
|
813
|
+
);
|
|
814
|
+
const path = pathFor(realmKey, environment);
|
|
815
|
+
let content: string | null;
|
|
816
|
+
try {
|
|
817
|
+
content = readPrivateStateFileIfPresent(
|
|
818
|
+
path,
|
|
819
|
+
MAX_ADMISSION_BYTES,
|
|
820
|
+
"web-session cleanup admission",
|
|
821
|
+
environment,
|
|
822
|
+
);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
"requested web-session cleanup admission is unreadable or unsafe; run ghostget doctor before continuing",
|
|
826
|
+
{ cause: error },
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
if (content === null) return null;
|
|
830
|
+
try {
|
|
831
|
+
const claim = parseWebSessionCleanupAdmissionClaim(
|
|
832
|
+
JSON.parse(content) as unknown,
|
|
833
|
+
);
|
|
834
|
+
if (
|
|
835
|
+
claim.realmKey !== realmKey
|
|
836
|
+
|| content !== `${canonicalJson(claim)}\n`
|
|
837
|
+
) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
"web-session cleanup admission does not match its canonical realm",
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return Object.freeze({
|
|
843
|
+
claim,
|
|
844
|
+
contentSha256: sha256(content),
|
|
845
|
+
});
|
|
846
|
+
} catch (error) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
"requested web-session cleanup admission is invalid; run ghostget doctor before continuing",
|
|
849
|
+
{ cause: error },
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function replaceClaim(
|
|
855
|
+
current: WebSessionCleanupAdmissionSnapshot,
|
|
856
|
+
claim: WebSessionCleanupAdmissionClaim,
|
|
857
|
+
environment: Environment,
|
|
858
|
+
): WebSessionCleanupAdmissionSnapshot {
|
|
859
|
+
const checked = claimSnapshot(current.claim);
|
|
860
|
+
if (checked.contentSha256 !== current.contentSha256) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
"web-session cleanup admission snapshot is not content-bound",
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
const next = claimSnapshot(claim);
|
|
866
|
+
if (
|
|
867
|
+
next.claim.realmKey !== checked.claim.realmKey
|
|
868
|
+
|| next.claim.runId !== checked.claim.runId
|
|
869
|
+
|| next.claim.owner.token !== checked.claim.owner.token
|
|
870
|
+
) {
|
|
871
|
+
throw new Error(
|
|
872
|
+
"web-session cleanup admission replacement changed its identity",
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (!writePrivateJsonIfUnchanged(
|
|
876
|
+
pathFor(checked.claim.realmKey, environment),
|
|
877
|
+
next.claim,
|
|
878
|
+
{ expectedCurrentContentSha256: checked.contentSha256 },
|
|
879
|
+
)) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
"web-session cleanup admission changed before containment update",
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
return next;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* A state-file CAS may commit and then report failure. Re-read the exact
|
|
889
|
+
* desired snapshot before propagating the error so callers never mistake a
|
|
890
|
+
* durable publication for an unpublished resource.
|
|
891
|
+
*/
|
|
892
|
+
function replaceClaimOrAdoptCommitted(
|
|
893
|
+
current: WebSessionCleanupAdmissionSnapshot,
|
|
894
|
+
claim: WebSessionCleanupAdmissionClaim,
|
|
895
|
+
environment: Environment,
|
|
896
|
+
afterCommitForTest?: () => void,
|
|
897
|
+
): WebSessionCleanupAdmissionSnapshot {
|
|
898
|
+
const desired = claimSnapshot(claim);
|
|
899
|
+
try {
|
|
900
|
+
const committed = replaceClaim(current, desired.claim, environment);
|
|
901
|
+
afterCommitForTest?.();
|
|
902
|
+
return committed;
|
|
903
|
+
} catch (error) {
|
|
904
|
+
let observed: WebSessionCleanupAdmissionSnapshot | null;
|
|
905
|
+
try {
|
|
906
|
+
observed = readWebSessionCleanupAdmission(
|
|
907
|
+
desired.claim.realmKey,
|
|
908
|
+
environment,
|
|
909
|
+
);
|
|
910
|
+
} catch (reconciliationError) {
|
|
911
|
+
throw new AggregateError(
|
|
912
|
+
[error, reconciliationError],
|
|
913
|
+
"web-session cleanup publication could not be reconciled",
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
if (observed?.contentSha256 === desired.contentSha256) {
|
|
917
|
+
return observed;
|
|
918
|
+
}
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function replaceContainment(
|
|
924
|
+
current: WebSessionCleanupAdmissionSnapshot,
|
|
925
|
+
containment: WebSessionCleanupAdmissionContainment,
|
|
926
|
+
environment: Environment,
|
|
927
|
+
): WebSessionCleanupAdmissionSnapshot {
|
|
928
|
+
return replaceClaim(
|
|
929
|
+
current,
|
|
930
|
+
Object.freeze({
|
|
931
|
+
...current.claim,
|
|
932
|
+
containment,
|
|
933
|
+
}),
|
|
934
|
+
environment,
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export type WebSessionCleanupAdmissionController = {
|
|
939
|
+
readonly current: WebSessionCleanupAdmissionSnapshot;
|
|
940
|
+
readonly registerCleanupBarrier: WebSessionCleanupBarrierRegistrar;
|
|
941
|
+
readonly closeRegistration: () => void;
|
|
942
|
+
readonly cleanupComplete: () => void;
|
|
943
|
+
readonly cleanupUnsafe: () => void;
|
|
944
|
+
readonly release: () => void;
|
|
945
|
+
readonly barriers: readonly Promise<void>[];
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
type TrackedCleanupBarrier = {
|
|
949
|
+
promise: Promise<void>;
|
|
950
|
+
status: "pending" | "fulfilled" | "rejected";
|
|
951
|
+
readonly resourceId: string;
|
|
952
|
+
reason?: unknown;
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
type WebSessionCleanupAdmissionControllerDependencies = {
|
|
956
|
+
/** Test-only seam that throws after one resource-state CAS commits. */
|
|
957
|
+
readonly afterResourceStateCommitForTest?: () => void;
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
function recoverableBrowserResource(
|
|
961
|
+
reason: unknown,
|
|
962
|
+
resource: WebSessionCleanupAdmissionResource,
|
|
963
|
+
): WebSessionCleanupAdmissionResource | null {
|
|
964
|
+
if (
|
|
965
|
+
!(reason instanceof PreservedBrowserArtifactsError)
|
|
966
|
+
|| reason.cleanupEvidence?.kind
|
|
967
|
+
!== "agent-browser-closed-artifacts-v1"
|
|
968
|
+
) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
let identity: BrowserCleanupResourceIdentity;
|
|
972
|
+
try {
|
|
973
|
+
identity = parseBrowserCleanupResourceIdentity(
|
|
974
|
+
reason.cleanupEvidence.resource,
|
|
975
|
+
);
|
|
976
|
+
} catch {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
if (
|
|
980
|
+
resource.status !== "unpublished"
|
|
981
|
+
&& (
|
|
982
|
+
resource.status !== "active"
|
|
983
|
+
|| resource.identity.kind === "local-cli-private-root-v1"
|
|
984
|
+
|| canonicalJson(identity) !== canonicalJson(resource.identity)
|
|
985
|
+
)
|
|
986
|
+
) return null;
|
|
987
|
+
return Object.freeze({
|
|
988
|
+
resourceId: resource.resourceId,
|
|
989
|
+
status: "browser-closed-artifacts",
|
|
990
|
+
identity,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function controller(
|
|
995
|
+
initial: WebSessionCleanupAdmissionSnapshot,
|
|
996
|
+
environment: Environment,
|
|
997
|
+
dependencies: WebSessionCleanupAdmissionControllerDependencies = {},
|
|
998
|
+
): WebSessionCleanupAdmissionController {
|
|
999
|
+
let current = claimSnapshot(initial.claim);
|
|
1000
|
+
if (current.contentSha256 !== initial.contentSha256) {
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
"web-session cleanup admission snapshot is not content-bound",
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
let accepting = true;
|
|
1006
|
+
let released = false;
|
|
1007
|
+
const barriers: TrackedCleanupBarrier[] = [];
|
|
1008
|
+
const result: WebSessionCleanupAdmissionController = {
|
|
1009
|
+
get current() {
|
|
1010
|
+
return current;
|
|
1011
|
+
},
|
|
1012
|
+
get barriers() {
|
|
1013
|
+
return Object.freeze(barriers.map((barrier) => barrier.promise));
|
|
1014
|
+
},
|
|
1015
|
+
registerCleanupBarrier: (barrier) => {
|
|
1016
|
+
if (!accepting) {
|
|
1017
|
+
throw new Error(
|
|
1018
|
+
"web-session cleanup admission registration is closed",
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
if (
|
|
1022
|
+
current.claim.containment.status !== "parent-owned"
|
|
1023
|
+
&& current.claim.containment.status !== "resource-active"
|
|
1024
|
+
) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
"web-session cleanup admission is not accepting resources",
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
if (current.claim.resources.length >= 8) {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
"web-session cleanup admission resource count exceeds its bound",
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
const resourceId = randomUUID();
|
|
1035
|
+
current = replaceClaim(
|
|
1036
|
+
current,
|
|
1037
|
+
Object.freeze({
|
|
1038
|
+
...current.claim,
|
|
1039
|
+
containment: Object.freeze({ status: "resource-active" }),
|
|
1040
|
+
resources: Object.freeze([
|
|
1041
|
+
...current.claim.resources,
|
|
1042
|
+
Object.freeze({
|
|
1043
|
+
resourceId,
|
|
1044
|
+
status: "unpublished" as const,
|
|
1045
|
+
}),
|
|
1046
|
+
]),
|
|
1047
|
+
}),
|
|
1048
|
+
environment,
|
|
1049
|
+
);
|
|
1050
|
+
const tracked: TrackedCleanupBarrier = {
|
|
1051
|
+
status: "pending",
|
|
1052
|
+
promise: Promise.resolve(),
|
|
1053
|
+
resourceId,
|
|
1054
|
+
};
|
|
1055
|
+
tracked.promise = Promise.resolve(barrier).then(
|
|
1056
|
+
() => {
|
|
1057
|
+
tracked.status = "fulfilled";
|
|
1058
|
+
},
|
|
1059
|
+
(cause: unknown) => {
|
|
1060
|
+
tracked.status = "rejected";
|
|
1061
|
+
tracked.reason = cause;
|
|
1062
|
+
throw cause;
|
|
1063
|
+
},
|
|
1064
|
+
);
|
|
1065
|
+
void tracked.promise.catch(() => undefined);
|
|
1066
|
+
barriers.push(tracked);
|
|
1067
|
+
const publishResource = (
|
|
1068
|
+
resourceValue: ProviderPluginCleanupResourceIdentity,
|
|
1069
|
+
): void => {
|
|
1070
|
+
const resourceIdentity =
|
|
1071
|
+
parseCleanupResourceIdentity(resourceValue);
|
|
1072
|
+
const resources = current.claim.resources.map((resource) => {
|
|
1073
|
+
if (resource.resourceId !== resourceId) return resource;
|
|
1074
|
+
if (resource.status === "unpublished") {
|
|
1075
|
+
return Object.freeze({
|
|
1076
|
+
resourceId,
|
|
1077
|
+
status: "active" as const,
|
|
1078
|
+
identity: resourceIdentity,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
if (
|
|
1082
|
+
resource.status === "active"
|
|
1083
|
+
&& canonicalJson(resource.identity)
|
|
1084
|
+
=== canonicalJson(resourceIdentity)
|
|
1085
|
+
) {
|
|
1086
|
+
return resource;
|
|
1087
|
+
}
|
|
1088
|
+
if (
|
|
1089
|
+
resource.status === "active"
|
|
1090
|
+
&& resource.identity.kind === "local-cli-private-root-v1"
|
|
1091
|
+
&& resourceIdentity.kind === "local-cli-private-root-v1"
|
|
1092
|
+
&& localCliCleanupResourceExtends(
|
|
1093
|
+
resource.identity,
|
|
1094
|
+
resourceIdentity,
|
|
1095
|
+
)
|
|
1096
|
+
) {
|
|
1097
|
+
return Object.freeze({
|
|
1098
|
+
resourceId,
|
|
1099
|
+
status: "active" as const,
|
|
1100
|
+
identity: resourceIdentity,
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
if (
|
|
1104
|
+
resource.status === "active"
|
|
1105
|
+
&& resource.identity.kind !== "local-cli-private-root-v1"
|
|
1106
|
+
&& resourceIdentity.kind !== "local-cli-private-root-v1"
|
|
1107
|
+
&& browserCleanupResourceExtends(
|
|
1108
|
+
resource.identity,
|
|
1109
|
+
resourceIdentity,
|
|
1110
|
+
)
|
|
1111
|
+
) {
|
|
1112
|
+
return Object.freeze({
|
|
1113
|
+
resourceId,
|
|
1114
|
+
status: "active" as const,
|
|
1115
|
+
identity: resourceIdentity,
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
throw new Error(
|
|
1119
|
+
"web-session cleanup resource identity changed after publication",
|
|
1120
|
+
);
|
|
1121
|
+
});
|
|
1122
|
+
current = replaceClaimOrAdoptCommitted(
|
|
1123
|
+
current,
|
|
1124
|
+
Object.freeze({
|
|
1125
|
+
...current.claim,
|
|
1126
|
+
resources: Object.freeze(resources),
|
|
1127
|
+
}),
|
|
1128
|
+
environment,
|
|
1129
|
+
dependencies.afterResourceStateCommitForTest,
|
|
1130
|
+
);
|
|
1131
|
+
};
|
|
1132
|
+
const exactPublishedBrowserIdentity = (
|
|
1133
|
+
resourceValue: BrowserCleanupResourceIdentity,
|
|
1134
|
+
status: "active" | "browser-quiescent-artifacts",
|
|
1135
|
+
): BrowserCleanupResourceIdentityV2 => {
|
|
1136
|
+
const identity = parseBrowserCleanupResourceIdentity(resourceValue);
|
|
1137
|
+
const selected = current.claim.resources.find((resource) =>
|
|
1138
|
+
resource.resourceId === resourceId
|
|
1139
|
+
);
|
|
1140
|
+
if (
|
|
1141
|
+
identity.kind !== "agent-browser-session-v2"
|
|
1142
|
+
|| selected?.status !== status
|
|
1143
|
+
|| canonicalJson(selected.identity) !== canonicalJson(identity)
|
|
1144
|
+
) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
"web-session cleanup browser journal changed resource identity",
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
return identity;
|
|
1150
|
+
};
|
|
1151
|
+
return Object.assign(publishResource, {
|
|
1152
|
+
markBrowserCleanupQuiescent: (
|
|
1153
|
+
resourceValue: BrowserCleanupResourceIdentity,
|
|
1154
|
+
): void => {
|
|
1155
|
+
const identity = exactPublishedBrowserIdentity(
|
|
1156
|
+
resourceValue,
|
|
1157
|
+
"active",
|
|
1158
|
+
);
|
|
1159
|
+
if (
|
|
1160
|
+
browserCleanupResourceRootStatus(identity, "artifacts") !== "match"
|
|
1161
|
+
|| browserCleanupResourceRootStatus(identity, "socket") !== "match"
|
|
1162
|
+
) {
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
"web-session cleanup browser roots changed before quiescence",
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
current = replaceBrowserResource(
|
|
1168
|
+
current,
|
|
1169
|
+
resourceId,
|
|
1170
|
+
identity,
|
|
1171
|
+
"browser-quiescent-artifacts",
|
|
1172
|
+
environment,
|
|
1173
|
+
dependencies.afterResourceStateCommitForTest,
|
|
1174
|
+
);
|
|
1175
|
+
},
|
|
1176
|
+
markBrowserCleanupRootRemoved: (
|
|
1177
|
+
resourceValue: BrowserCleanupResourceIdentity,
|
|
1178
|
+
root: BrowserCleanupRootName,
|
|
1179
|
+
): void => {
|
|
1180
|
+
const identity = exactPublishedBrowserIdentity(
|
|
1181
|
+
resourceValue,
|
|
1182
|
+
"browser-quiescent-artifacts",
|
|
1183
|
+
);
|
|
1184
|
+
const selected = current.claim.resources.find((resource) =>
|
|
1185
|
+
resource.resourceId === resourceId
|
|
1186
|
+
);
|
|
1187
|
+
const expectedRoot = selected?.status === "browser-quiescent-artifacts"
|
|
1188
|
+
? (["artifacts", "socket"] as const)[selected.removedRoots.length]
|
|
1189
|
+
: undefined;
|
|
1190
|
+
const companionRoot = root === "artifacts" ? "socket" : "artifacts";
|
|
1191
|
+
const companionStatus = root === "artifacts" ? "match" : "absent";
|
|
1192
|
+
if (
|
|
1193
|
+
expectedRoot !== root
|
|
1194
|
+
|| browserCleanupResourceRootStatus(identity, root) !== "absent"
|
|
1195
|
+
|| browserCleanupResourceRootStatus(identity, companionRoot)
|
|
1196
|
+
!== companionStatus
|
|
1197
|
+
) {
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
"web-session cleanup browser root removal is not exact",
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
current = journalBrowserRootRemoved(
|
|
1203
|
+
current,
|
|
1204
|
+
resourceId,
|
|
1205
|
+
root,
|
|
1206
|
+
environment,
|
|
1207
|
+
dependencies.afterResourceStateCommitForTest,
|
|
1208
|
+
);
|
|
1209
|
+
},
|
|
1210
|
+
});
|
|
1211
|
+
},
|
|
1212
|
+
closeRegistration: () => {
|
|
1213
|
+
accepting = false;
|
|
1214
|
+
},
|
|
1215
|
+
cleanupComplete: () => {
|
|
1216
|
+
if (current.claim.containment.status === "cleanup-complete") return;
|
|
1217
|
+
if (accepting) {
|
|
1218
|
+
throw new Error(
|
|
1219
|
+
"web-session cleanup admission registration must close before cleanup completion",
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
if (current.claim.containment.status === "cleanup-unsafe") {
|
|
1223
|
+
throw new Error(
|
|
1224
|
+
"web-session cleanup admission cannot complete after becoming unsafe",
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
if (barriers.some((barrier) => barrier.status === "pending")) {
|
|
1228
|
+
throw new Error(
|
|
1229
|
+
"web-session cleanup admission barriers have not all settled",
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
if (barriers.some((barrier) => barrier.status === "rejected")) {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
"web-session cleanup admission cannot complete after a rejected barrier",
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
current = replaceContainment(
|
|
1238
|
+
current,
|
|
1239
|
+
Object.freeze({ status: "cleanup-complete" }),
|
|
1240
|
+
environment,
|
|
1241
|
+
);
|
|
1242
|
+
},
|
|
1243
|
+
cleanupUnsafe: () => {
|
|
1244
|
+
if (current.claim.containment.status === "cleanup-unsafe") return;
|
|
1245
|
+
if (accepting) {
|
|
1246
|
+
throw new Error(
|
|
1247
|
+
"web-session cleanup admission registration must close before cleanup is marked unsafe",
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
if (current.claim.containment.status === "cleanup-complete") {
|
|
1251
|
+
throw new Error(
|
|
1252
|
+
"web-session cleanup admission became unsafe after completion",
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
const byResourceId = new Map(
|
|
1256
|
+
barriers.map((barrier) => [barrier.resourceId, barrier] as const),
|
|
1257
|
+
);
|
|
1258
|
+
const resources = current.claim.resources.flatMap((resource) => {
|
|
1259
|
+
const tracked = byResourceId.get(resource.resourceId);
|
|
1260
|
+
if (tracked?.status === "fulfilled") return [];
|
|
1261
|
+
if (tracked?.status === "rejected") {
|
|
1262
|
+
return [
|
|
1263
|
+
recoverableBrowserResource(tracked.reason, resource)
|
|
1264
|
+
?? resource,
|
|
1265
|
+
];
|
|
1266
|
+
}
|
|
1267
|
+
return [resource];
|
|
1268
|
+
});
|
|
1269
|
+
current = replaceClaim(
|
|
1270
|
+
current,
|
|
1271
|
+
Object.freeze({
|
|
1272
|
+
...current.claim,
|
|
1273
|
+
containment: Object.freeze({ status: "cleanup-unsafe" }),
|
|
1274
|
+
resources: Object.freeze(resources),
|
|
1275
|
+
}),
|
|
1276
|
+
environment,
|
|
1277
|
+
);
|
|
1278
|
+
},
|
|
1279
|
+
release: () => {
|
|
1280
|
+
if (released) return;
|
|
1281
|
+
if (current.claim.containment.status !== "cleanup-complete") {
|
|
1282
|
+
throw new Error(
|
|
1283
|
+
"web-session cleanup admission cannot be released before cleanup completion",
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
removePrivateStateFileIfUnchanged(
|
|
1287
|
+
pathFor(current.claim.realmKey, environment),
|
|
1288
|
+
{ expectedCurrentContentSha256: current.contentSha256 },
|
|
1289
|
+
environment,
|
|
1290
|
+
);
|
|
1291
|
+
// Cleanup-complete is itself sufficient retry proof. Another invoker or
|
|
1292
|
+
// doctor may already have retired this exact claim and published a
|
|
1293
|
+
// successor; a false CAS must never remove that successor or turn the
|
|
1294
|
+
// completed operation into a failure.
|
|
1295
|
+
released = true;
|
|
1296
|
+
},
|
|
1297
|
+
};
|
|
1298
|
+
return Object.freeze(result);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function createClaim(
|
|
1302
|
+
identity: WebSessionCleanupAdmissionIdentity,
|
|
1303
|
+
acquiredAt: Date,
|
|
1304
|
+
): WebSessionCleanupAdmissionClaim {
|
|
1305
|
+
const realmKey = webSessionCleanupRealmKey(
|
|
1306
|
+
identity.surfaceId,
|
|
1307
|
+
identity.authId,
|
|
1308
|
+
);
|
|
1309
|
+
const processIdentity = currentProcessStartIdentity();
|
|
1310
|
+
return parseWebSessionCleanupAdmissionClaim({
|
|
1311
|
+
schemaVersion: ADMISSION_SCHEMA_VERSION,
|
|
1312
|
+
realmKey,
|
|
1313
|
+
runId: identity.runId,
|
|
1314
|
+
pluginId: identity.pluginId,
|
|
1315
|
+
pluginVersion: identity.pluginVersion,
|
|
1316
|
+
pluginImplementationHash: identity.pluginImplementationHash,
|
|
1317
|
+
adapterId: identity.adapterId,
|
|
1318
|
+
adapterHash: identity.adapterHash,
|
|
1319
|
+
surfaceId: identity.surfaceId,
|
|
1320
|
+
authId: identity.authId,
|
|
1321
|
+
authHash: identity.authHash,
|
|
1322
|
+
...(identity.transport === undefined
|
|
1323
|
+
|| identity.executionIdentityHash === undefined
|
|
1324
|
+
? {}
|
|
1325
|
+
: {
|
|
1326
|
+
transport: identity.transport,
|
|
1327
|
+
executionIdentityHash: identity.executionIdentityHash,
|
|
1328
|
+
}),
|
|
1329
|
+
owner: {
|
|
1330
|
+
pid: process.pid,
|
|
1331
|
+
token: randomUUID(),
|
|
1332
|
+
...processIdentity,
|
|
1333
|
+
},
|
|
1334
|
+
acquiredAt: acquiredAt.toISOString(),
|
|
1335
|
+
containment: { status: "parent-owned" },
|
|
1336
|
+
resources: [],
|
|
1337
|
+
recovery: { status: "idle" },
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
type SameBootCleanupUnsafeRecovery =
|
|
1342
|
+
| "repaired"
|
|
1343
|
+
| "live-owner"
|
|
1344
|
+
| "recovery-active"
|
|
1345
|
+
| "owner-unknown"
|
|
1346
|
+
| "proof-unavailable"
|
|
1347
|
+
| "artifact-conflict"
|
|
1348
|
+
| "claim-conflict";
|
|
1349
|
+
|
|
1350
|
+
type CleanupRecoveryLeaseResult =
|
|
1351
|
+
| {
|
|
1352
|
+
readonly status: "acquired";
|
|
1353
|
+
readonly snapshot: WebSessionCleanupAdmissionSnapshot;
|
|
1354
|
+
}
|
|
1355
|
+
| {
|
|
1356
|
+
readonly status:
|
|
1357
|
+
| "live-owner"
|
|
1358
|
+
| "recovery-active"
|
|
1359
|
+
| "owner-unknown"
|
|
1360
|
+
| "claim-conflict";
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
function acquireCleanupRecoveryLease(
|
|
1364
|
+
entry: WebSessionCleanupAdmissionSnapshot,
|
|
1365
|
+
environment: Environment,
|
|
1366
|
+
inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
1367
|
+
acquiredAt = new Date(),
|
|
1368
|
+
): CleanupRecoveryLeaseResult {
|
|
1369
|
+
const ownerStatus = inspectOwner(entry.claim.owner);
|
|
1370
|
+
if (ownerStatus === "exact-live-owner") {
|
|
1371
|
+
return { status: "live-owner" };
|
|
1372
|
+
}
|
|
1373
|
+
if (ownerStatus === "unknown") {
|
|
1374
|
+
return { status: "owner-unknown" };
|
|
1375
|
+
}
|
|
1376
|
+
if (
|
|
1377
|
+
entry.claim.schemaVersion === ADMISSION_SCHEMA_VERSION
|
|
1378
|
+
&& entry.claim.recovery.status === "active"
|
|
1379
|
+
) {
|
|
1380
|
+
const recoveryOwnerStatus = inspectOwner(entry.claim.recovery.owner);
|
|
1381
|
+
if (recoveryOwnerStatus === "exact-live-owner") {
|
|
1382
|
+
return { status: "recovery-active" };
|
|
1383
|
+
}
|
|
1384
|
+
if (recoveryOwnerStatus === "unknown") {
|
|
1385
|
+
return { status: "owner-unknown" };
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const processIdentity = currentProcessStartIdentity();
|
|
1389
|
+
try {
|
|
1390
|
+
const snapshot = replaceClaim(
|
|
1391
|
+
entry,
|
|
1392
|
+
parseWebSessionCleanupAdmissionClaim({
|
|
1393
|
+
...entry.claim,
|
|
1394
|
+
schemaVersion: ADMISSION_SCHEMA_VERSION,
|
|
1395
|
+
recovery: {
|
|
1396
|
+
status: "active",
|
|
1397
|
+
owner: {
|
|
1398
|
+
pid: process.pid,
|
|
1399
|
+
token: randomUUID(),
|
|
1400
|
+
...processIdentity,
|
|
1401
|
+
},
|
|
1402
|
+
acquiredAt: acquiredAt.toISOString(),
|
|
1403
|
+
},
|
|
1404
|
+
}),
|
|
1405
|
+
environment,
|
|
1406
|
+
);
|
|
1407
|
+
return { status: "acquired", snapshot };
|
|
1408
|
+
} catch {
|
|
1409
|
+
return { status: "claim-conflict" };
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function exactCurrentClaim(
|
|
1414
|
+
expected: WebSessionCleanupAdmissionSnapshot,
|
|
1415
|
+
environment: Environment,
|
|
1416
|
+
): WebSessionCleanupAdmissionSnapshot | null {
|
|
1417
|
+
const current = readWebSessionCleanupAdmission(
|
|
1418
|
+
expected.claim.realmKey,
|
|
1419
|
+
environment,
|
|
1420
|
+
);
|
|
1421
|
+
return current !== null
|
|
1422
|
+
&& current.contentSha256 === expected.contentSha256
|
|
1423
|
+
? current
|
|
1424
|
+
: null;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function releaseCleanupRecoveryLease(
|
|
1428
|
+
expected: WebSessionCleanupAdmissionSnapshot,
|
|
1429
|
+
environment: Environment,
|
|
1430
|
+
): boolean {
|
|
1431
|
+
if (
|
|
1432
|
+
expected.claim.schemaVersion !== ADMISSION_SCHEMA_VERSION
|
|
1433
|
+
|| expected.claim.recovery.status !== "active"
|
|
1434
|
+
) return false;
|
|
1435
|
+
try {
|
|
1436
|
+
replaceClaim(
|
|
1437
|
+
expected,
|
|
1438
|
+
parseWebSessionCleanupAdmissionClaim({
|
|
1439
|
+
...expected.claim,
|
|
1440
|
+
recovery: { status: "idle" },
|
|
1441
|
+
}),
|
|
1442
|
+
environment,
|
|
1443
|
+
);
|
|
1444
|
+
return true;
|
|
1445
|
+
} catch {
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function replaceBrowserResource(
|
|
1451
|
+
current: WebSessionCleanupAdmissionSnapshot,
|
|
1452
|
+
resourceId: string,
|
|
1453
|
+
identity: BrowserCleanupResourceIdentityV2,
|
|
1454
|
+
status:
|
|
1455
|
+
| "active"
|
|
1456
|
+
| "browser-closed-artifacts"
|
|
1457
|
+
| "browser-quiescent-artifacts",
|
|
1458
|
+
environment: Environment,
|
|
1459
|
+
afterCommitForTest?: () => void,
|
|
1460
|
+
): WebSessionCleanupAdmissionSnapshot {
|
|
1461
|
+
let found = false;
|
|
1462
|
+
const resources = current.claim.resources.map((resource) => {
|
|
1463
|
+
if (resource.resourceId !== resourceId) return resource;
|
|
1464
|
+
if (
|
|
1465
|
+
resource.status === "unpublished"
|
|
1466
|
+
|| resource.status === "browser-quiescent-artifacts"
|
|
1467
|
+
|| resource.identity.kind === "local-cli-private-root-v1"
|
|
1468
|
+
|| !browserCleanupResourceExtends(resource.identity, identity)
|
|
1469
|
+
) {
|
|
1470
|
+
throw new Error(
|
|
1471
|
+
"web-session cleanup browser recovery changed resource identity",
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
found = true;
|
|
1475
|
+
return status === "browser-quiescent-artifacts"
|
|
1476
|
+
? Object.freeze({
|
|
1477
|
+
resourceId,
|
|
1478
|
+
status,
|
|
1479
|
+
identity,
|
|
1480
|
+
removedRoots: Object.freeze([] as BrowserCleanupRootName[]),
|
|
1481
|
+
})
|
|
1482
|
+
: Object.freeze({ resourceId, status, identity });
|
|
1483
|
+
});
|
|
1484
|
+
if (!found) {
|
|
1485
|
+
throw new Error("web-session cleanup browser recovery resource is absent");
|
|
1486
|
+
}
|
|
1487
|
+
return replaceClaimOrAdoptCommitted(
|
|
1488
|
+
current,
|
|
1489
|
+
parseWebSessionCleanupAdmissionClaim({
|
|
1490
|
+
...current.claim,
|
|
1491
|
+
resources,
|
|
1492
|
+
}),
|
|
1493
|
+
environment,
|
|
1494
|
+
afterCommitForTest,
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function journalBrowserRootRemoved(
|
|
1499
|
+
current: WebSessionCleanupAdmissionSnapshot,
|
|
1500
|
+
resourceId: string,
|
|
1501
|
+
root: BrowserCleanupRootName,
|
|
1502
|
+
environment: Environment,
|
|
1503
|
+
afterCommitForTest?: () => void,
|
|
1504
|
+
): WebSessionCleanupAdmissionSnapshot {
|
|
1505
|
+
let found = false;
|
|
1506
|
+
const resources = current.claim.resources.map((resource) => {
|
|
1507
|
+
if (resource.resourceId !== resourceId) return resource;
|
|
1508
|
+
if (resource.status !== "browser-quiescent-artifacts") {
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
"web-session cleanup browser root removal is not quiescent",
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
const expected = (["artifacts", "socket"] as const)[
|
|
1514
|
+
resource.removedRoots.length
|
|
1515
|
+
];
|
|
1516
|
+
if (expected !== root) {
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
"web-session cleanup browser root removal order changed",
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
found = true;
|
|
1522
|
+
return Object.freeze({
|
|
1523
|
+
...resource,
|
|
1524
|
+
removedRoots: Object.freeze([
|
|
1525
|
+
...resource.removedRoots,
|
|
1526
|
+
root,
|
|
1527
|
+
] as BrowserCleanupRootName[]),
|
|
1528
|
+
});
|
|
1529
|
+
});
|
|
1530
|
+
if (!found) {
|
|
1531
|
+
throw new Error("web-session cleanup browser recovery resource is absent");
|
|
1532
|
+
}
|
|
1533
|
+
return replaceClaimOrAdoptCommitted(
|
|
1534
|
+
current,
|
|
1535
|
+
parseWebSessionCleanupAdmissionClaim({
|
|
1536
|
+
...current.claim,
|
|
1537
|
+
resources,
|
|
1538
|
+
}),
|
|
1539
|
+
environment,
|
|
1540
|
+
afterCommitForTest,
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
type BrowserRootFinalization =
|
|
1545
|
+
| {
|
|
1546
|
+
readonly status: "complete";
|
|
1547
|
+
readonly snapshot: WebSessionCleanupAdmissionSnapshot;
|
|
1548
|
+
}
|
|
1549
|
+
| {
|
|
1550
|
+
readonly status: "artifact-conflict" | "claim-conflict";
|
|
1551
|
+
readonly snapshot: WebSessionCleanupAdmissionSnapshot;
|
|
1552
|
+
};
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Resume an identity-bound root-removal journal. Full quiescence is refreshed
|
|
1556
|
+
* immediately before artifacts removal. The still-bound socket then supports
|
|
1557
|
+
* a session/owner/CDP reproof immediately before its own removal. A missing
|
|
1558
|
+
* root is accepted only after the exact resource entered the durable
|
|
1559
|
+
* quiescent phase; a replacement remains a hard conflict.
|
|
1560
|
+
*/
|
|
1561
|
+
async function finalizeRecoveredBrowserResource(
|
|
1562
|
+
initial: WebSessionCleanupAdmissionSnapshot,
|
|
1563
|
+
resourceId: string,
|
|
1564
|
+
environment: Environment,
|
|
1565
|
+
lifecycle: AgentBrowserLifecycleDependencies,
|
|
1566
|
+
): Promise<BrowserRootFinalization> {
|
|
1567
|
+
let current = initial;
|
|
1568
|
+
for (const rootName of ["artifacts", "socket"] as const) {
|
|
1569
|
+
const selected = current.claim.resources.find((resource) =>
|
|
1570
|
+
resource.resourceId === resourceId
|
|
1571
|
+
);
|
|
1572
|
+
if (
|
|
1573
|
+
selected === undefined
|
|
1574
|
+
|| selected.status !== "browser-quiescent-artifacts"
|
|
1575
|
+
) {
|
|
1576
|
+
return { status: "claim-conflict", snapshot: current };
|
|
1577
|
+
}
|
|
1578
|
+
const rootStatus = browserCleanupResourceRootStatus(
|
|
1579
|
+
selected.identity,
|
|
1580
|
+
rootName,
|
|
1581
|
+
);
|
|
1582
|
+
if (selected.removedRoots.includes(rootName)) {
|
|
1583
|
+
if (rootStatus !== "absent") {
|
|
1584
|
+
return { status: "artifact-conflict", snapshot: current };
|
|
1585
|
+
}
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (exactCurrentClaim(current, environment) === null) {
|
|
1589
|
+
return { status: "claim-conflict", snapshot: current };
|
|
1590
|
+
}
|
|
1591
|
+
if (rootStatus === "conflict") {
|
|
1592
|
+
return { status: "artifact-conflict", snapshot: current };
|
|
1593
|
+
}
|
|
1594
|
+
if (rootStatus === "match") {
|
|
1595
|
+
try {
|
|
1596
|
+
if (rootName === "artifacts") {
|
|
1597
|
+
if (
|
|
1598
|
+
browserCleanupResourceRootStatus(
|
|
1599
|
+
selected.identity,
|
|
1600
|
+
"socket",
|
|
1601
|
+
) !== "match"
|
|
1602
|
+
) {
|
|
1603
|
+
return { status: "artifact-conflict", snapshot: current };
|
|
1604
|
+
}
|
|
1605
|
+
await refreshBrowserCleanupResourceQuiescence(
|
|
1606
|
+
selected.identity,
|
|
1607
|
+
lifecycle,
|
|
1608
|
+
);
|
|
1609
|
+
} else {
|
|
1610
|
+
if (
|
|
1611
|
+
browserCleanupResourceRootStatus(
|
|
1612
|
+
selected.identity,
|
|
1613
|
+
"artifacts",
|
|
1614
|
+
) !== "absent"
|
|
1615
|
+
) {
|
|
1616
|
+
return { status: "artifact-conflict", snapshot: current };
|
|
1617
|
+
}
|
|
1618
|
+
await reproveBrowserCleanupAfterArtifactsRemoval(
|
|
1619
|
+
selected.identity,
|
|
1620
|
+
lifecycle,
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
} catch {
|
|
1624
|
+
return { status: "artifact-conflict", snapshot: current };
|
|
1625
|
+
}
|
|
1626
|
+
if (exactCurrentClaim(current, environment) === null) {
|
|
1627
|
+
return { status: "claim-conflict", snapshot: current };
|
|
1628
|
+
}
|
|
1629
|
+
if (
|
|
1630
|
+
browserCleanupResourceRootStatus(
|
|
1631
|
+
selected.identity,
|
|
1632
|
+
rootName,
|
|
1633
|
+
) !== "match"
|
|
1634
|
+
) {
|
|
1635
|
+
return { status: "artifact-conflict", snapshot: current };
|
|
1636
|
+
}
|
|
1637
|
+
const root = rootName === "artifacts"
|
|
1638
|
+
? {
|
|
1639
|
+
path: selected.identity.artifactsDirectory,
|
|
1640
|
+
identity: selected.identity.artifactsDirectoryIdentity,
|
|
1641
|
+
}
|
|
1642
|
+
: {
|
|
1643
|
+
path: selected.identity.socketDirectory,
|
|
1644
|
+
identity: selected.identity.socketDirectoryIdentity,
|
|
1645
|
+
};
|
|
1646
|
+
try {
|
|
1647
|
+
if (!removePrivateDirectoryTree(root.path, {
|
|
1648
|
+
device: root.identity.device,
|
|
1649
|
+
inode: root.identity.inode,
|
|
1650
|
+
birthtimeNs: root.identity.birthtimeNs,
|
|
1651
|
+
})) {
|
|
1652
|
+
return { status: "artifact-conflict", snapshot: current };
|
|
1653
|
+
}
|
|
1654
|
+
} catch {
|
|
1655
|
+
return { status: "artifact-conflict", snapshot: current };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (exactCurrentClaim(current, environment) === null) {
|
|
1659
|
+
return { status: "claim-conflict", snapshot: current };
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
current = journalBrowserRootRemoved(
|
|
1663
|
+
current,
|
|
1664
|
+
resourceId,
|
|
1665
|
+
rootName,
|
|
1666
|
+
environment,
|
|
1667
|
+
);
|
|
1668
|
+
} catch {
|
|
1669
|
+
return { status: "claim-conflict", snapshot: current };
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return { status: "complete", snapshot: current };
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
async function recoverBrowserCleanupUnsafe(
|
|
1676
|
+
entry: WebSessionCleanupAdmissionSnapshot,
|
|
1677
|
+
environment: Environment,
|
|
1678
|
+
inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
1679
|
+
lifecycle: AgentBrowserLifecycleDependencies,
|
|
1680
|
+
): Promise<SameBootCleanupUnsafeRecovery> {
|
|
1681
|
+
const lease = acquireCleanupRecoveryLease(
|
|
1682
|
+
entry,
|
|
1683
|
+
environment,
|
|
1684
|
+
inspectOwner,
|
|
1685
|
+
);
|
|
1686
|
+
if (lease.status !== "acquired") return lease.status;
|
|
1687
|
+
let current = lease.snapshot;
|
|
1688
|
+
if (
|
|
1689
|
+
current.claim.resources.length === 0
|
|
1690
|
+
|| current.claim.resources.some((resource) =>
|
|
1691
|
+
resource.status === "unpublished"
|
|
1692
|
+
|| resource.identity.kind === "local-cli-private-root-v1"
|
|
1693
|
+
)
|
|
1694
|
+
) {
|
|
1695
|
+
releaseCleanupRecoveryLease(current, environment);
|
|
1696
|
+
return "proof-unavailable";
|
|
1697
|
+
}
|
|
1698
|
+
for (const initialResource of current.claim.resources) {
|
|
1699
|
+
const selected = current.claim.resources.find((resource) =>
|
|
1700
|
+
resource.resourceId === initialResource.resourceId
|
|
1701
|
+
);
|
|
1702
|
+
if (selected === undefined || selected.status === "unpublished") {
|
|
1703
|
+
releaseCleanupRecoveryLease(current, environment);
|
|
1704
|
+
return "claim-conflict";
|
|
1705
|
+
}
|
|
1706
|
+
if (selected.status === "browser-quiescent-artifacts") {
|
|
1707
|
+
const finalization = await finalizeRecoveredBrowserResource(
|
|
1708
|
+
current,
|
|
1709
|
+
selected.resourceId,
|
|
1710
|
+
environment,
|
|
1711
|
+
lifecycle,
|
|
1712
|
+
);
|
|
1713
|
+
current = finalization.snapshot;
|
|
1714
|
+
if (finalization.status !== "complete") {
|
|
1715
|
+
releaseCleanupRecoveryLease(current, environment);
|
|
1716
|
+
return finalization.status;
|
|
1717
|
+
}
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
if (selected.identity.kind === "local-cli-private-root-v1") {
|
|
1721
|
+
releaseCleanupRecoveryLease(current, environment);
|
|
1722
|
+
return "proof-unavailable";
|
|
1723
|
+
}
|
|
1724
|
+
try {
|
|
1725
|
+
if (exactCurrentClaim(current, environment) === null) {
|
|
1726
|
+
return "claim-conflict";
|
|
1727
|
+
}
|
|
1728
|
+
let pinned: BrowserCleanupResourceIdentityV2;
|
|
1729
|
+
if (selected.identity.kind === "agent-browser-session-v1") {
|
|
1730
|
+
try {
|
|
1731
|
+
pinned = await adoptLiveLegacyBrowserCleanupResource(
|
|
1732
|
+
selected.identity,
|
|
1733
|
+
lifecycle,
|
|
1734
|
+
);
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
if (!releaseCleanupRecoveryLease(current, environment)) {
|
|
1737
|
+
return "claim-conflict";
|
|
1738
|
+
}
|
|
1739
|
+
return error instanceof AgentBrowserLiveControlUnavailableError
|
|
1740
|
+
? "proof-unavailable"
|
|
1741
|
+
: "artifact-conflict";
|
|
1742
|
+
}
|
|
1743
|
+
} else {
|
|
1744
|
+
pinned = selected.identity.phase === "launch-intent"
|
|
1745
|
+
&& selected.identity.control === null
|
|
1746
|
+
? await bindLiveAgentBrowserCleanupResource(
|
|
1747
|
+
selected.identity,
|
|
1748
|
+
lifecycle,
|
|
1749
|
+
)
|
|
1750
|
+
: selected.identity;
|
|
1751
|
+
}
|
|
1752
|
+
if (
|
|
1753
|
+
selected.identity.kind !== "agent-browser-session-v2"
|
|
1754
|
+
|| canonicalJson(selected.identity) !== canonicalJson(pinned)
|
|
1755
|
+
) {
|
|
1756
|
+
current = replaceBrowserResource(
|
|
1757
|
+
current,
|
|
1758
|
+
selected.resourceId,
|
|
1759
|
+
pinned,
|
|
1760
|
+
selected.status,
|
|
1761
|
+
environment,
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
if (exactCurrentClaim(current, environment) === null) {
|
|
1765
|
+
return "claim-conflict";
|
|
1766
|
+
}
|
|
1767
|
+
if (pinned.phase === "prepared") {
|
|
1768
|
+
await provePreparedAgentBrowserCleanupResourceQuiescent(
|
|
1769
|
+
pinned,
|
|
1770
|
+
lifecycle,
|
|
1771
|
+
);
|
|
1772
|
+
} else {
|
|
1773
|
+
await recoverPinnedAgentBrowserCleanupResource(pinned, lifecycle);
|
|
1774
|
+
}
|
|
1775
|
+
if (exactCurrentClaim(current, environment) === null) {
|
|
1776
|
+
return "claim-conflict";
|
|
1777
|
+
}
|
|
1778
|
+
current = replaceBrowserResource(
|
|
1779
|
+
current,
|
|
1780
|
+
selected.resourceId,
|
|
1781
|
+
pinned,
|
|
1782
|
+
"browser-quiescent-artifacts",
|
|
1783
|
+
environment,
|
|
1784
|
+
);
|
|
1785
|
+
} catch {
|
|
1786
|
+
return releaseCleanupRecoveryLease(current, environment)
|
|
1787
|
+
? "artifact-conflict"
|
|
1788
|
+
: "claim-conflict";
|
|
1789
|
+
}
|
|
1790
|
+
const finalization = await finalizeRecoveredBrowserResource(
|
|
1791
|
+
current,
|
|
1792
|
+
selected.resourceId,
|
|
1793
|
+
environment,
|
|
1794
|
+
lifecycle,
|
|
1795
|
+
);
|
|
1796
|
+
current = finalization.snapshot;
|
|
1797
|
+
if (finalization.status !== "complete") {
|
|
1798
|
+
releaseCleanupRecoveryLease(current, environment);
|
|
1799
|
+
return finalization.status;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return removePrivateStateFileIfUnchanged(
|
|
1803
|
+
pathFor(current.claim.realmKey, environment),
|
|
1804
|
+
{ expectedCurrentContentSha256: current.contentSha256 },
|
|
1805
|
+
environment,
|
|
1806
|
+
)
|
|
1807
|
+
? "repaired"
|
|
1808
|
+
: "claim-conflict";
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function removeRecoverableLocalCliRoots(
|
|
1812
|
+
claim: WebSessionCleanupAdmissionClaim,
|
|
1813
|
+
inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
1814
|
+
): SameBootCleanupUnsafeRecovery {
|
|
1815
|
+
if (
|
|
1816
|
+
claim.resources.length === 0
|
|
1817
|
+
|| claim.resources.some((resource) =>
|
|
1818
|
+
resource.status !== "active"
|
|
1819
|
+
|| resource.identity.kind !== "local-cli-private-root-v1"
|
|
1820
|
+
)
|
|
1821
|
+
) {
|
|
1822
|
+
return "proof-unavailable";
|
|
1823
|
+
}
|
|
1824
|
+
const owner = inspectOwner(claim.owner);
|
|
1825
|
+
if (owner === "exact-live-owner") return "live-owner";
|
|
1826
|
+
if (owner === "unknown") return "owner-unknown";
|
|
1827
|
+
for (const resource of claim.resources) {
|
|
1828
|
+
if (
|
|
1829
|
+
resource.status !== "active"
|
|
1830
|
+
|| resource.identity.kind !== "local-cli-private-root-v1"
|
|
1831
|
+
) {
|
|
1832
|
+
return "proof-unavailable";
|
|
1833
|
+
}
|
|
1834
|
+
const groupStatus = localCliCleanupProcessGroupStatus(
|
|
1835
|
+
resource.identity,
|
|
1836
|
+
inspectOwner,
|
|
1837
|
+
);
|
|
1838
|
+
if (groupStatus === "active") return "live-owner";
|
|
1839
|
+
if (groupStatus !== "quiescent") return "proof-unavailable";
|
|
1840
|
+
}
|
|
1841
|
+
try {
|
|
1842
|
+
for (const resource of claim.resources) {
|
|
1843
|
+
if (
|
|
1844
|
+
resource.status !== "active"
|
|
1845
|
+
|| resource.identity.kind !== "local-cli-private-root-v1"
|
|
1846
|
+
) {
|
|
1847
|
+
return "proof-unavailable";
|
|
1848
|
+
}
|
|
1849
|
+
removePrivateDirectoryTree(resource.identity.root.path, {
|
|
1850
|
+
device: resource.identity.root.device,
|
|
1851
|
+
inode: resource.identity.root.inode,
|
|
1852
|
+
birthtimeNs: resource.identity.root.birthtimeNs,
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
} catch {
|
|
1856
|
+
return "artifact-conflict";
|
|
1857
|
+
}
|
|
1858
|
+
return "repaired";
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function removePriorBootQuiescentLocalCliRoots(
|
|
1862
|
+
claim: WebSessionCleanupAdmissionClaim,
|
|
1863
|
+
): boolean {
|
|
1864
|
+
if (
|
|
1865
|
+
claim.resources.length === 0
|
|
1866
|
+
|| claim.resources.some((resource) =>
|
|
1867
|
+
resource.status !== "active"
|
|
1868
|
+
|| resource.identity.kind !== "local-cli-private-root-v1"
|
|
1869
|
+
)
|
|
1870
|
+
) return false;
|
|
1871
|
+
try {
|
|
1872
|
+
for (const resource of claim.resources) {
|
|
1873
|
+
if (
|
|
1874
|
+
resource.status !== "active"
|
|
1875
|
+
|| resource.identity.kind !== "local-cli-private-root-v1"
|
|
1876
|
+
) {
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
removePrivateDirectoryTree(resource.identity.root.path, {
|
|
1880
|
+
device: resource.identity.root.device,
|
|
1881
|
+
inode: resource.identity.root.inode,
|
|
1882
|
+
birthtimeNs: resource.identity.root.birthtimeNs,
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
return true;
|
|
1886
|
+
} catch {
|
|
1887
|
+
return false;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function recoverSameBootCleanupUnsafe(
|
|
1892
|
+
entry: WebSessionCleanupAdmissionSnapshot,
|
|
1893
|
+
environment: Environment,
|
|
1894
|
+
inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
1895
|
+
): SameBootCleanupUnsafeRecovery {
|
|
1896
|
+
if (
|
|
1897
|
+
entry.claim.containment.status !== "cleanup-unsafe"
|
|
1898
|
+
&& entry.claim.containment.status !== "resource-active"
|
|
1899
|
+
) {
|
|
1900
|
+
return "proof-unavailable";
|
|
1901
|
+
}
|
|
1902
|
+
if (entry.claim.transport !== "local-cli") {
|
|
1903
|
+
return "proof-unavailable";
|
|
1904
|
+
}
|
|
1905
|
+
const lease = acquireCleanupRecoveryLease(
|
|
1906
|
+
entry,
|
|
1907
|
+
environment,
|
|
1908
|
+
inspectOwner,
|
|
1909
|
+
);
|
|
1910
|
+
if (lease.status !== "acquired") return lease.status;
|
|
1911
|
+
const localRecovery = removeRecoverableLocalCliRoots(
|
|
1912
|
+
lease.snapshot.claim,
|
|
1913
|
+
inspectOwner,
|
|
1914
|
+
);
|
|
1915
|
+
if (localRecovery !== "repaired") {
|
|
1916
|
+
return releaseCleanupRecoveryLease(lease.snapshot, environment)
|
|
1917
|
+
? localRecovery
|
|
1918
|
+
: "claim-conflict";
|
|
1919
|
+
}
|
|
1920
|
+
return removePrivateStateFileIfUnchanged(
|
|
1921
|
+
pathFor(lease.snapshot.claim.realmKey, environment),
|
|
1922
|
+
{ expectedCurrentContentSha256: lease.snapshot.contentSha256 },
|
|
1923
|
+
environment,
|
|
1924
|
+
)
|
|
1925
|
+
? "repaired"
|
|
1926
|
+
: "claim-conflict";
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function blockedAdmissionGuidance(
|
|
1930
|
+
claim: WebSessionCleanupAdmissionClaim,
|
|
1931
|
+
recovery: SameBootCleanupUnsafeRecovery | null,
|
|
1932
|
+
): string {
|
|
1933
|
+
const realm = `${claim.surfaceId}/${claim.authId}`;
|
|
1934
|
+
const transport = claim.transport === "local-cli"
|
|
1935
|
+
? "local CLI"
|
|
1936
|
+
: "authenticated web";
|
|
1937
|
+
if (recovery === "live-owner") {
|
|
1938
|
+
return `${transport} auth realm ${realm} has active or cleanup-unsafe state still owned by an active run; wait for it to finish`;
|
|
1939
|
+
}
|
|
1940
|
+
if (recovery === "recovery-active") {
|
|
1941
|
+
return `${transport} auth realm ${realm} cleanup recovery is active; wait for ghostget doctor to finish`;
|
|
1942
|
+
}
|
|
1943
|
+
if (recovery === "owner-unknown") {
|
|
1944
|
+
return `${transport} auth realm ${realm} owner liveness cannot be proved; run ghostget doctor again after process inspection becomes available`;
|
|
1945
|
+
}
|
|
1946
|
+
if (recovery === "artifact-conflict") {
|
|
1947
|
+
return `${transport} auth realm ${realm} has identity-changed private cleanup artifacts; retry is unsafe until exact session recovery succeeds`;
|
|
1948
|
+
}
|
|
1949
|
+
if (recovery === "claim-conflict") {
|
|
1950
|
+
return `${transport} auth realm ${realm} cleanup recovery changed concurrently; run ghostget doctor before retrying`;
|
|
1951
|
+
}
|
|
1952
|
+
if (
|
|
1953
|
+
claim.containment.status === "cleanup-unsafe"
|
|
1954
|
+
&& recovery === "proof-unavailable"
|
|
1955
|
+
) {
|
|
1956
|
+
return `${transport} auth realm ${realm} has cleanup-unsafe state without exact quiescence evidence; run ghostget doctor to recover the exact private session before retrying`;
|
|
1957
|
+
}
|
|
1958
|
+
if (claim.containment.status === "resource-active") {
|
|
1959
|
+
return `${transport} auth realm ${realm} has a resource-active crash boundary; run ghostget doctor to recover the exact private session before retrying`;
|
|
1960
|
+
}
|
|
1961
|
+
return `${transport} auth realm ${realm} has active or cleanup-unsafe state; wait for the active run, or run ghostget doctor before retrying`;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
export class WebSessionCleanupAdmissionBlockedError extends Error {
|
|
1965
|
+
constructor(message: string, cause?: unknown) {
|
|
1966
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
1967
|
+
this.name = "WebSessionCleanupAdmissionBlockedError";
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function cleanupAdmissionBlocked(error: unknown): WebSessionCleanupAdmissionBlockedError {
|
|
1972
|
+
if (error instanceof WebSessionCleanupAdmissionBlockedError) return error;
|
|
1973
|
+
return new WebSessionCleanupAdmissionBlockedError(
|
|
1974
|
+
error instanceof Error
|
|
1975
|
+
? error.message
|
|
1976
|
+
: "web-session cleanup admission could not be acquired",
|
|
1977
|
+
error,
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function acquireWebSessionCleanupAdmissionCore(
|
|
1982
|
+
claim: WebSessionCleanupAdmissionClaim,
|
|
1983
|
+
environment: Environment,
|
|
1984
|
+
dependencies: WebSessionCleanupAdmissionControllerDependencies = {},
|
|
1985
|
+
): WebSessionCleanupAdmissionController {
|
|
1986
|
+
const admissionDirectory = directory(environment);
|
|
1987
|
+
ensurePrivateStateDirectory(admissionDirectory, environment);
|
|
1988
|
+
for (
|
|
1989
|
+
let attempt = 0;
|
|
1990
|
+
attempt < MAX_ACQUISITION_ATTEMPTS;
|
|
1991
|
+
attempt += 1
|
|
1992
|
+
) {
|
|
1993
|
+
const existing = readWebSessionCleanupAdmission(
|
|
1994
|
+
claim.realmKey,
|
|
1995
|
+
environment,
|
|
1996
|
+
);
|
|
1997
|
+
if (existing !== null) {
|
|
1998
|
+
if (
|
|
1999
|
+
existing.claim.schemaVersion === ADMISSION_SCHEMA_VERSION
|
|
2000
|
+
&& existing.claim.recovery.status === "active"
|
|
2001
|
+
) {
|
|
2002
|
+
throw new WebSessionCleanupAdmissionBlockedError(
|
|
2003
|
+
blockedAdmissionGuidance(existing.claim, "recovery-active"),
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
const containment = existing.claim.containment.status;
|
|
2007
|
+
const currentBootId = claim.owner.bootId;
|
|
2008
|
+
let sameBootUnsafeRecovery:
|
|
2009
|
+
| SameBootCleanupUnsafeRecovery
|
|
2010
|
+
| null = null;
|
|
2011
|
+
if (
|
|
2012
|
+
existing.claim.transport === "local-cli"
|
|
2013
|
+
&& (
|
|
2014
|
+
containment === "cleanup-unsafe"
|
|
2015
|
+
|| containment === "resource-active"
|
|
2016
|
+
)
|
|
2017
|
+
&& existing.claim.owner.bootId === currentBootId
|
|
2018
|
+
) {
|
|
2019
|
+
sameBootUnsafeRecovery = recoverSameBootCleanupUnsafe(
|
|
2020
|
+
existing,
|
|
2021
|
+
environment,
|
|
2022
|
+
processOwnerStatus,
|
|
2023
|
+
);
|
|
2024
|
+
if (sameBootUnsafeRecovery === "repaired") continue;
|
|
2025
|
+
if (sameBootUnsafeRecovery === "claim-conflict") continue;
|
|
2026
|
+
}
|
|
2027
|
+
const priorBootQuiescent = (
|
|
2028
|
+
containment === "resource-active"
|
|
2029
|
+
|| containment === "cleanup-unsafe"
|
|
2030
|
+
)
|
|
2031
|
+
&& existing.claim.owner.bootId !== currentBootId;
|
|
2032
|
+
const automaticallyRepairable = containment === "cleanup-complete"
|
|
2033
|
+
|| (
|
|
2034
|
+
containment === "parent-owned"
|
|
2035
|
+
&& processOwnerStatus(existing.claim.owner)
|
|
2036
|
+
=== "different-or-dead"
|
|
2037
|
+
)
|
|
2038
|
+
|| (
|
|
2039
|
+
(
|
|
2040
|
+
containment === "resource-active"
|
|
2041
|
+
|| containment === "cleanup-unsafe"
|
|
2042
|
+
)
|
|
2043
|
+
&& existing.claim.transport === "local-cli"
|
|
2044
|
+
&& existing.claim.owner.bootId !== currentBootId
|
|
2045
|
+
);
|
|
2046
|
+
if (automaticallyRepairable) {
|
|
2047
|
+
if (
|
|
2048
|
+
priorBootQuiescent
|
|
2049
|
+
&& !removePriorBootQuiescentLocalCliRoots(existing.claim)
|
|
2050
|
+
) {
|
|
2051
|
+
throw new WebSessionCleanupAdmissionBlockedError(
|
|
2052
|
+
blockedAdmissionGuidance(existing.claim, "artifact-conflict"),
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
removePrivateStateFileIfUnchanged(
|
|
2056
|
+
pathFor(claim.realmKey, environment),
|
|
2057
|
+
{ expectedCurrentContentSha256: existing.contentSha256 },
|
|
2058
|
+
environment,
|
|
2059
|
+
);
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
throw new WebSessionCleanupAdmissionBlockedError(
|
|
2063
|
+
blockedAdmissionGuidance(
|
|
2064
|
+
existing.claim,
|
|
2065
|
+
sameBootUnsafeRecovery,
|
|
2066
|
+
),
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
let created: { readonly created: boolean };
|
|
2070
|
+
try {
|
|
2071
|
+
created = createPrivateJsonIfAbsent(
|
|
2072
|
+
pathFor(claim.realmKey, environment),
|
|
2073
|
+
claim,
|
|
2074
|
+
{ environment },
|
|
2075
|
+
);
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
if (
|
|
2078
|
+
error instanceof Error
|
|
2079
|
+
&& error.message.includes("state file mutation is already active")
|
|
2080
|
+
) {
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
throw error;
|
|
2084
|
+
}
|
|
2085
|
+
if (created.created) {
|
|
2086
|
+
return controller(claimSnapshot(claim), environment, dependencies);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
throw new WebSessionCleanupAdmissionBlockedError(
|
|
2090
|
+
`${claim.transport === "local-cli" ? "local CLI" : "authenticated web"} auth realm ${claim.surfaceId}/${claim.authId} cleanup admission could not be acquired`,
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
export function acquireWebSessionCleanupAdmission(
|
|
2095
|
+
identity: WebSessionCleanupAdmissionIdentity,
|
|
2096
|
+
environment: Environment = process.env,
|
|
2097
|
+
acquiredAt = new Date(),
|
|
2098
|
+
dependencies: WebSessionCleanupAdmissionControllerDependencies = {},
|
|
2099
|
+
): WebSessionCleanupAdmissionController {
|
|
2100
|
+
const claim = createClaim(identity, acquiredAt);
|
|
2101
|
+
try {
|
|
2102
|
+
return acquireWebSessionCleanupAdmissionCore(
|
|
2103
|
+
claim,
|
|
2104
|
+
environment,
|
|
2105
|
+
dependencies,
|
|
2106
|
+
);
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
throw cleanupAdmissionBlocked(error);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
/**
|
|
2113
|
+
* The operation result and cleanup safety are independent. The operation may
|
|
2114
|
+
* return a truthful zero-dispatch failure after its cleanup barrier rejects;
|
|
2115
|
+
* the retained durable admission, rather than a fabricated dispatch state,
|
|
2116
|
+
* prevents another run in that auth realm.
|
|
2117
|
+
*/
|
|
2118
|
+
export async function withWebSessionCleanupAdmission<T>(
|
|
2119
|
+
identity: WebSessionCleanupAdmissionIdentity,
|
|
2120
|
+
environment: Environment,
|
|
2121
|
+
operation: (
|
|
2122
|
+
registerCleanupBarrier: WebSessionCleanupBarrierRegistrar,
|
|
2123
|
+
) => Promise<T>,
|
|
2124
|
+
acquiredAt = new Date(),
|
|
2125
|
+
onAdmissionBlocked?: (
|
|
2126
|
+
error: WebSessionCleanupAdmissionBlockedError,
|
|
2127
|
+
) => Promise<T>,
|
|
2128
|
+
): Promise<T> {
|
|
2129
|
+
let admission: WebSessionCleanupAdmissionController;
|
|
2130
|
+
try {
|
|
2131
|
+
admission = acquireWebSessionCleanupAdmission(
|
|
2132
|
+
identity,
|
|
2133
|
+
environment,
|
|
2134
|
+
acquiredAt,
|
|
2135
|
+
);
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
if (
|
|
2138
|
+
error instanceof WebSessionCleanupAdmissionBlockedError
|
|
2139
|
+
&& onAdmissionBlocked !== undefined
|
|
2140
|
+
) {
|
|
2141
|
+
return onAdmissionBlocked(error);
|
|
2142
|
+
}
|
|
2143
|
+
throw error;
|
|
2144
|
+
}
|
|
2145
|
+
let outcome:
|
|
2146
|
+
| { readonly status: "fulfilled"; readonly value: T }
|
|
2147
|
+
| { readonly status: "rejected"; readonly reason: unknown };
|
|
2148
|
+
try {
|
|
2149
|
+
try {
|
|
2150
|
+
outcome = {
|
|
2151
|
+
status: "fulfilled",
|
|
2152
|
+
value: await operation(admission.registerCleanupBarrier),
|
|
2153
|
+
};
|
|
2154
|
+
} catch (reason) {
|
|
2155
|
+
outcome = { status: "rejected", reason };
|
|
2156
|
+
}
|
|
2157
|
+
admission.closeRegistration();
|
|
2158
|
+
const cleanup = await Promise.allSettled(admission.barriers);
|
|
2159
|
+
if (cleanup.some((result) => result.status === "rejected")) {
|
|
2160
|
+
admission.cleanupUnsafe();
|
|
2161
|
+
} else {
|
|
2162
|
+
admission.cleanupComplete();
|
|
2163
|
+
admission.release();
|
|
2164
|
+
}
|
|
2165
|
+
if (outcome.status === "rejected") throw outcome.reason;
|
|
2166
|
+
return outcome.value;
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
admission.closeRegistration();
|
|
2169
|
+
if (admission.current.claim.containment.status === "parent-owned") {
|
|
2170
|
+
admission.cleanupComplete();
|
|
2171
|
+
admission.release();
|
|
2172
|
+
}
|
|
2173
|
+
throw error;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
export async function recoverWebSessionCleanupAdmissions(
|
|
2178
|
+
environment: Environment = process.env,
|
|
2179
|
+
): Promise<WebSessionCleanupAdmissionRecoveryReport> {
|
|
2180
|
+
return recoverWebSessionCleanupAdmissionsCore(
|
|
2181
|
+
environment,
|
|
2182
|
+
{
|
|
2183
|
+
inspectOwner: processOwnerStatus,
|
|
2184
|
+
currentBootId: currentProcessStartIdentity().bootId,
|
|
2185
|
+
browserLifecycle: {},
|
|
2186
|
+
},
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
export type WebSessionCleanupAdmissionRecoveryDependencies = {
|
|
2191
|
+
readonly inspectOwner: (
|
|
2192
|
+
owner: ProcessOwnerIdentity,
|
|
2193
|
+
) => ProcessOwnerStatus;
|
|
2194
|
+
readonly currentBootId: string;
|
|
2195
|
+
readonly browserLifecycle: AgentBrowserLifecycleDependencies;
|
|
2196
|
+
};
|
|
2197
|
+
|
|
2198
|
+
/** Internal deterministic seam; public doctor always supplies kernel probes. */
|
|
2199
|
+
export async function recoverWebSessionCleanupAdmissionsCore(
|
|
2200
|
+
environment: Environment,
|
|
2201
|
+
dependencies: WebSessionCleanupAdmissionRecoveryDependencies,
|
|
2202
|
+
): Promise<WebSessionCleanupAdmissionRecoveryReport> {
|
|
2203
|
+
const {
|
|
2204
|
+
inspectOwner,
|
|
2205
|
+
currentBootId,
|
|
2206
|
+
browserLifecycle,
|
|
2207
|
+
} = dependencies;
|
|
2208
|
+
digest(currentBootId, "current boot identity");
|
|
2209
|
+
const entries = listWebSessionCleanupAdmissions(environment);
|
|
2210
|
+
const issues: WebSessionCleanupAdmissionRecoveryIssue[] = [];
|
|
2211
|
+
let repaired = 0;
|
|
2212
|
+
let active = 0;
|
|
2213
|
+
let retained = 0;
|
|
2214
|
+
let invalid = 0;
|
|
2215
|
+
for (const entry of entries) {
|
|
2216
|
+
if ("invalid" in entry) {
|
|
2217
|
+
invalid += 1;
|
|
2218
|
+
issues.push({
|
|
2219
|
+
coordinate: entry.coordinate,
|
|
2220
|
+
kind: "invalid-admission",
|
|
2221
|
+
});
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
const { claim } = entry;
|
|
2225
|
+
if (
|
|
2226
|
+
claim.schemaVersion === ADMISSION_SCHEMA_VERSION
|
|
2227
|
+
&& claim.recovery.status === "active"
|
|
2228
|
+
) {
|
|
2229
|
+
const recoveryOwner = inspectOwner(claim.recovery.owner);
|
|
2230
|
+
if (recoveryOwner === "exact-live-owner") {
|
|
2231
|
+
active += 1;
|
|
2232
|
+
issues.push({
|
|
2233
|
+
coordinate: claim.realmKey,
|
|
2234
|
+
kind: "recovery-active",
|
|
2235
|
+
});
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
if (recoveryOwner === "unknown") {
|
|
2239
|
+
retained += 1;
|
|
2240
|
+
issues.push({
|
|
2241
|
+
coordinate: claim.realmKey,
|
|
2242
|
+
kind: "owner-unknown",
|
|
2243
|
+
});
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const containment = claim.containment.status;
|
|
2248
|
+
let repairable = containment === "cleanup-complete";
|
|
2249
|
+
if (containment === "parent-owned") {
|
|
2250
|
+
const owner = inspectOwner(claim.owner);
|
|
2251
|
+
if (owner === "exact-live-owner") {
|
|
2252
|
+
active += 1;
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
if (owner === "unknown") {
|
|
2256
|
+
retained += 1;
|
|
2257
|
+
issues.push({
|
|
2258
|
+
coordinate: claim.realmKey,
|
|
2259
|
+
kind: "owner-unknown",
|
|
2260
|
+
});
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
repairable = true;
|
|
2264
|
+
} else if (
|
|
2265
|
+
containment === "resource-active"
|
|
2266
|
+
|| containment === "cleanup-unsafe"
|
|
2267
|
+
) {
|
|
2268
|
+
const hasPublishedLocalCliResource = claim.resources.some((resource) =>
|
|
2269
|
+
resource.status !== "unpublished"
|
|
2270
|
+
&& resource.identity.kind === "local-cli-private-root-v1"
|
|
2271
|
+
);
|
|
2272
|
+
const hasPublishedBrowserResource = claim.resources.some((resource) =>
|
|
2273
|
+
resource.status !== "unpublished"
|
|
2274
|
+
&& resource.identity.kind !== "local-cli-private-root-v1"
|
|
2275
|
+
);
|
|
2276
|
+
if (
|
|
2277
|
+
hasPublishedLocalCliResource
|
|
2278
|
+
&& hasPublishedBrowserResource
|
|
2279
|
+
) {
|
|
2280
|
+
retained += 1;
|
|
2281
|
+
issues.push({
|
|
2282
|
+
coordinate: claim.realmKey,
|
|
2283
|
+
kind: "cleanup-unsafe",
|
|
2284
|
+
});
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
const recovery = hasPublishedLocalCliResource
|
|
2288
|
+
? recoverSameBootCleanupUnsafe(
|
|
2289
|
+
entry,
|
|
2290
|
+
environment,
|
|
2291
|
+
inspectOwner,
|
|
2292
|
+
)
|
|
2293
|
+
: claim.owner.bootId !== currentBootId
|
|
2294
|
+
? "proof-unavailable"
|
|
2295
|
+
: await recoverBrowserCleanupUnsafe(
|
|
2296
|
+
entry,
|
|
2297
|
+
environment,
|
|
2298
|
+
inspectOwner,
|
|
2299
|
+
browserLifecycle,
|
|
2300
|
+
);
|
|
2301
|
+
if (recovery === "repaired") {
|
|
2302
|
+
repaired += 1;
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
retained += 1;
|
|
2306
|
+
issues.push({
|
|
2307
|
+
coordinate: claim.realmKey,
|
|
2308
|
+
kind: recovery === "owner-unknown"
|
|
2309
|
+
? "owner-unknown"
|
|
2310
|
+
: recovery === "live-owner"
|
|
2311
|
+
? "resource-active"
|
|
2312
|
+
: recovery === "recovery-active"
|
|
2313
|
+
? "recovery-active"
|
|
2314
|
+
: recovery === "artifact-conflict"
|
|
2315
|
+
|| recovery === "claim-conflict"
|
|
2316
|
+
? "recovery-conflict"
|
|
2317
|
+
: "cleanup-unsafe",
|
|
2318
|
+
});
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
if (!repairable) continue;
|
|
2322
|
+
if (removePrivateStateFileIfUnchanged(
|
|
2323
|
+
pathFor(claim.realmKey, environment),
|
|
2324
|
+
{ expectedCurrentContentSha256: entry.contentSha256 },
|
|
2325
|
+
environment,
|
|
2326
|
+
)) {
|
|
2327
|
+
repaired += 1;
|
|
2328
|
+
} else {
|
|
2329
|
+
retained += 1;
|
|
2330
|
+
issues.push({
|
|
2331
|
+
coordinate: claim.realmKey,
|
|
2332
|
+
kind: "recovery-conflict",
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return Object.freeze({
|
|
2337
|
+
scanned: entries.length,
|
|
2338
|
+
repaired,
|
|
2339
|
+
active,
|
|
2340
|
+
retained,
|
|
2341
|
+
invalid,
|
|
2342
|
+
issues: Object.freeze(issues),
|
|
2343
|
+
});
|
|
2344
|
+
}
|