@amg555/frameforge-mcp 2.7.6 → 2.7.7
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/plugins/video-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/video-pipeline/skills/.upstream-skills.json +1 -1
- package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +1 -1
- package/scripts/deep-validate.js +2 -2
- package/src/mcp-server/server.js +5 -5
- package/src/skills/sync-upstream.js +3 -3
- package/.agent/skills/ai-video-editor/README.md +0 -32
- package/.agent/skills/ai-video-editor/SKILL.md +0 -152
- package/.agent/skills/ai-video-editor/assets/fonts/fonts.css +0 -2409
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/anton-400.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/archivo-800.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/cabin-sketch-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/dm-sans-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/fraunces-600.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/fraunces-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/fredoka-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/inter-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/inter-800.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/inter-tight-800.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/kalam-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/noto-sans-devanagari-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-400-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-500-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-600-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-700-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-800-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/engine/hidpi.js +0 -117
- package/.agent/skills/ai-video-editor/engine/page.html +0 -43
- package/.agent/skills/ai-video-editor/engine/player.js +0 -145
- package/.agent/skills/ai-video-editor/references/example-story-warm-canvas.js +0 -95
- package/.agent/skills/ai-video-editor/references/story-guide.md +0 -112
- package/.agent/skills/ai-video-editor/requirements.txt +0 -4
- package/.agent/skills/ai-video-editor/styles/chalk-talk/base.js +0 -490
- package/.agent/skills/ai-video-editor/styles/chalk-talk/style.json +0 -14
- package/.agent/skills/ai-video-editor/styles/chalk-talk/style.md +0 -41
- package/.agent/skills/ai-video-editor/styles/clean-tech/base.js +0 -447
- package/.agent/skills/ai-video-editor/styles/clean-tech/style.json +0 -15
- package/.agent/skills/ai-video-editor/styles/clean-tech/style.md +0 -38
- package/.agent/skills/ai-video-editor/styles/creator-motion-graphics/base.js +0 -698
- package/.agent/skills/ai-video-editor/styles/creator-motion-graphics/style.json +0 -16
- package/.agent/skills/ai-video-editor/styles/creator-motion-graphics/style.md +0 -39
- package/.agent/skills/ai-video-editor/styles/data-story/base.js +0 -432
- package/.agent/skills/ai-video-editor/styles/data-story/style.json +0 -14
- package/.agent/skills/ai-video-editor/styles/data-story/style.md +0 -38
- package/.agent/skills/ai-video-editor/styles/index.json +0 -103
- package/.agent/skills/ai-video-editor/styles/pop-info/base.js +0 -570
- package/.agent/skills/ai-video-editor/styles/pop-info/style.json +0 -13
- package/.agent/skills/ai-video-editor/styles/pop-info/style.md +0 -37
- package/.agent/skills/ai-video-editor/styles/software-walkthrough/base.js +0 -544
- package/.agent/skills/ai-video-editor/styles/software-walkthrough/style.json +0 -15
- package/.agent/skills/ai-video-editor/styles/software-walkthrough/style.md +0 -38
- package/.agent/skills/ai-video-editor/styles/warm-canvas/base.js +0 -610
- package/.agent/skills/ai-video-editor/styles/warm-canvas/style.json +0 -14
- package/.agent/skills/ai-video-editor/styles/warm-canvas/style.md +0 -37
- package/.agent/skills/faceless-shorts/README.md +0 -34
- package/.agent/skills/faceless-shorts/SKILL.md +0 -124
- package/.agent/skills/faceless-shorts/styles/deep-dive/STYLE.md +0 -58
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/cues.json +0 -140
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/lines.json +0 -67
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/words.json +0 -614
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/words.txt +0 -102
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/fonts/Inter-ExtraBold.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/index.html +0 -16
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/script.json +0 -27
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/sheet.html +0 -10
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/captions.js +0 -68
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/engine.js +0 -65
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/human.js +0 -128
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/look2d.js +0 -33
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/sdf.js +0 -79
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/sets.js +0 -107
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/main.js +0 -56
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/sheet.js +0 -79
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/shots.js +0 -136
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/LICENSE-three.txt +0 -21
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/environments/RoomEnvironment.js +0 -144
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/BokehPass.js +0 -141
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/EffectComposer.js +0 -231
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/MaskPass.js +0 -104
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/OutputPass.js +0 -97
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/Pass.js +0 -95
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/RenderPass.js +0 -99
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/ShaderPass.js +0 -77
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/UnrealBloomPass.js +0 -415
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/BokehShader.js +0 -145
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/CopyShader.js +0 -45
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/LuminosityHighPassShader.js +0 -64
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/OutputShader.js +0 -85
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/three.module.js +0 -6
- package/.agent/skills/faceless-shorts/styles/deep-dive/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/inside-out/STYLE.md +0 -97
- package/.agent/skills/faceless-shorts/styles/inside-out/example/data/cues.json +0 -178
- package/.agent/skills/faceless-shorts/styles/inside-out/example/data/lines.json +0 -73
- package/.agent/skills/faceless-shorts/styles/inside-out/example/data/words.json +0 -662
- package/.agent/skills/faceless-shorts/styles/inside-out/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/inside-out/example/index.html +0 -16
- package/.agent/skills/faceless-shorts/styles/inside-out/example/script.json +0 -26
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/captions.js +0 -68
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/kit.js +0 -345
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/main.js +0 -62
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/shots.js +0 -195
- package/.agent/skills/faceless-shorts/styles/inside-out/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/inside-out/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/inside-out/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/night-file/STYLE.md +0 -36
- package/.agent/skills/faceless-shorts/styles/night-file/example/data/cues.json +0 -146
- package/.agent/skills/faceless-shorts/styles/night-file/example/data/lines.json +0 -73
- package/.agent/skills/faceless-shorts/styles/night-file/example/data/words.json +0 -572
- package/.agent/skills/faceless-shorts/styles/night-file/example/data/words.txt +0 -95
- package/.agent/skills/faceless-shorts/styles/night-file/example/fonts/FreeMonoBold.ttf +0 -0
- package/.agent/skills/faceless-shorts/styles/night-file/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/night-file/example/index.html +0 -16
- package/.agent/skills/faceless-shorts/styles/night-file/example/script.json +0 -26
- package/.agent/skills/faceless-shorts/styles/night-file/example/sheet.html +0 -11
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/captions.js +0 -49
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/engine.js +0 -65
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/human.js +0 -128
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/kit.js +0 -151
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/sdf.js +0 -79
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/sets3d.js +0 -106
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/main.js +0 -56
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/sheet.js +0 -86
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/shots.js +0 -130
- package/.agent/skills/faceless-shorts/styles/night-file/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/night-file/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/LICENSE-three.txt +0 -21
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/environments/RoomEnvironment.js +0 -144
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/BokehPass.js +0 -141
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/EffectComposer.js +0 -231
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/MaskPass.js +0 -104
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/OutputPass.js +0 -97
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/Pass.js +0 -95
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/RenderPass.js +0 -99
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/ShaderPass.js +0 -77
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/UnrealBloomPass.js +0 -415
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/BokehShader.js +0 -145
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/CopyShader.js +0 -45
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/LuminosityHighPassShader.js +0 -64
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/OutputShader.js +0 -85
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/three.module.js +0 -6
- package/.agent/skills/faceless-shorts/styles/night-file/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/paper-atlas/STYLE.md +0 -30
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/cues.json +0 -167
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/lines.json +0 -67
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/words.json +0 -614
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/words.txt +0 -102
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/fonts/Caladea-Bold.ttf +0 -0
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/index.html +0 -13
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/script.json +0 -26
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/sheet.html +0 -10
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/captions.js +0 -47
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/kit.js +0 -152
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/main.js +0 -62
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/sheet.js +0 -61
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/shots.js +0 -139
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/paper-atlas/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/stick-story/STYLE.md +0 -32
- package/.agent/skills/faceless-shorts/styles/stick-story/example/data/cues.json +0 -182
- package/.agent/skills/faceless-shorts/styles/stick-story/example/data/lines.json +0 -79
- package/.agent/skills/faceless-shorts/styles/stick-story/example/data/words.json +0 -662
- package/.agent/skills/faceless-shorts/styles/stick-story/example/data/words.txt +0 -110
- package/.agent/skills/faceless-shorts/styles/stick-story/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/stick-story/example/fonts/Inter-ExtraBold.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/stick-story/example/index.html +0 -12
- package/.agent/skills/faceless-shorts/styles/stick-story/example/script.json +0 -27
- package/.agent/skills/faceless-shorts/styles/stick-story/example/sheet.html +0 -9
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/captions.js +0 -47
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/kit.js +0 -166
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/main.js +0 -62
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/sheet.js +0 -64
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/shots.js +0 -148
- package/.agent/skills/faceless-shorts/styles/stick-story/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/stick-story/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/stick-story/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/tall-tale/STYLE.md +0 -51
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/cues.json +0 -271
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/lines.json +0 -127
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/words.json +0 -908
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/words.txt +0 -151
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/fonts/Caladea-Bold.ttf +0 -0
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/fonts/Inter-ExtraBold.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/index.html +0 -17
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/script.json +0 -37
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/sheet.html +0 -11
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/captions.js +0 -56
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/engine.js +0 -65
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/props.js +0 -166
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/toon.js +0 -352
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/world.js +0 -340
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/main.js +0 -56
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/sheet.js +0 -99
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/shots.js +0 -377
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/tools/mix.py +0 -204
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/LICENSE-three.txt +0 -21
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/environments/RoomEnvironment.js +0 -144
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/BokehPass.js +0 -141
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/EffectComposer.js +0 -231
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/MaskPass.js +0 -104
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/OutputPass.js +0 -97
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/Pass.js +0 -95
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/RenderPass.js +0 -99
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/ShaderPass.js +0 -77
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/UnrealBloomPass.js +0 -415
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/BokehShader.js +0 -145
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/CopyShader.js +0 -45
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/LuminosityHighPassShader.js +0 -64
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/OutputShader.js +0 -85
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/three.module.js +0 -6
- package/.agent/skills/faceless-shorts/styles/tall-tale/style-sheet.png +0 -0
- package/.agent/skills/gstack-autoplan/SKILL.md +0 -972
- package/.agent/skills/gstack-autoplan/SKILL.md.tmpl +0 -468
- package/.agent/skills/gstack-autoplan/bin/guard-journal.ts +0 -115
- package/.agent/skills/gstack-autoplan/bin/guard-log.ts +0 -40
- package/.agent/skills/gstack-autoplan/bin/guard-reasons.ts +0 -92
- package/.agent/skills/gstack-autoplan/bin/owned-read.ts +0 -72
- package/.agent/skills/gstack-autoplan/bin/phase-publication-hook +0 -15
- package/.agent/skills/gstack-autoplan/bin/phase-publication-hook.ts +0 -769
- package/.agent/skills/gstack-autoplan/sections/ceo-phase.md +0 -195
- package/.agent/skills/gstack-autoplan/sections/ceo-phase.md.tmpl +0 -147
- package/.agent/skills/gstack-autoplan/sections/design-phase.md +0 -128
- package/.agent/skills/gstack-autoplan/sections/design-phase.md.tmpl +0 -82
- package/.agent/skills/gstack-autoplan/sections/dx-phase.md +0 -155
- package/.agent/skills/gstack-autoplan/sections/dx-phase.md.tmpl +0 -109
- package/.agent/skills/gstack-autoplan/sections/eng-phase.md +0 -164
- package/.agent/skills/gstack-autoplan/sections/eng-phase.md.tmpl +0 -116
- package/.agent/skills/gstack-autoplan/sections/manifest.json +0 -50
- package/.agent/skills/gstack-autoplan/sections/phase-close.md +0 -70
- package/.agent/skills/gstack-autoplan/sections/phase-close.md.tmpl +0 -68
- package/.agent/skills/gstack-autoplan/sections/tasks-aggregator.md +0 -71
- package/.agent/skills/gstack-autoplan/sections/tasks-aggregator.md.tmpl +0 -1
- package/.agent/skills/gstack-autoplan/sections/unattended.md +0 -11
- package/.agent/skills/gstack-autoplan/sections/unattended.md.tmpl +0 -9
- package/.agent/skills/gstack-benchmark/SKILL.md +0 -456
- package/.agent/skills/gstack-benchmark/SKILL.md.tmpl +0 -249
- package/.agent/skills/gstack-benchmark-models/SKILL.md +0 -285
- package/.agent/skills/gstack-benchmark-models/SKILL.md.tmpl +0 -155
- package/.agent/skills/gstack-browse/PLAN-snapshot-dropdown-interactive.md +0 -102
- package/.agent/skills/gstack-browse/SKILL.md +0 -470
- package/.agent/skills/gstack-browse/SKILL.md.tmpl +0 -129
- package/.agent/skills/gstack-browse/bin/find-browse +0 -21
- package/.agent/skills/gstack-browse/bin/remote-slug +0 -18
- package/.agent/skills/gstack-browse/sections/command-list.md +0 -176
- package/.agent/skills/gstack-browse/sections/command-list.md.tmpl +0 -7
- package/.agent/skills/gstack-browse/sections/manifest.json +0 -14
- package/.agent/skills/gstack-browse/src/activity.ts +0 -209
- package/.agent/skills/gstack-browse/src/audit.ts +0 -69
- package/.agent/skills/gstack-browse/src/browse-client.ts +0 -264
- package/.agent/skills/gstack-browse/src/browser-manager.ts +0 -2334
- package/.agent/skills/gstack-browse/src/browser-skill-commands.ts +0 -466
- package/.agent/skills/gstack-browse/src/browser-skill-write.ts +0 -216
- package/.agent/skills/gstack-browse/src/browser-skills.ts +0 -420
- package/.agent/skills/gstack-browse/src/buffers.ts +0 -137
- package/.agent/skills/gstack-browse/src/bun-child-config.ts +0 -35
- package/.agent/skills/gstack-browse/src/bun-polyfill.cjs +0 -276
- package/.agent/skills/gstack-browse/src/cdp-allowlist.ts +0 -235
- package/.agent/skills/gstack-browse/src/cdp-bridge.ts +0 -180
- package/.agent/skills/gstack-browse/src/cdp-commands.ts +0 -64
- package/.agent/skills/gstack-browse/src/cdp-inspector.ts +0 -824
- package/.agent/skills/gstack-browse/src/chromium-profiles.ts +0 -212
- package/.agent/skills/gstack-browse/src/claude-bin.ts +0 -2
- package/.agent/skills/gstack-browse/src/cli.ts +0 -2254
- package/.agent/skills/gstack-browse/src/commands.ts +0 -296
- package/.agent/skills/gstack-browse/src/config.ts +0 -307
- package/.agent/skills/gstack-browse/src/content-security.ts +0 -373
- package/.agent/skills/gstack-browse/src/cookie-auth-verification.ts +0 -225
- package/.agent/skills/gstack-browse/src/cookie-database.ts +0 -73
- package/.agent/skills/gstack-browse/src/cookie-import-browser.ts +0 -929
- package/.agent/skills/gstack-browse/src/cookie-import-native-integrity.ts +0 -95
- package/.agent/skills/gstack-browse/src/cookie-import-native-job.ts +0 -161
- package/.agent/skills/gstack-browse/src/cookie-import-native-qualification.json +0 -1
- package/.agent/skills/gstack-browse/src/cookie-import-native-worker.ts +0 -268
- package/.agent/skills/gstack-browse/src/cookie-import-native.ts +0 -166
- package/.agent/skills/gstack-browse/src/cookie-import-operation.ts +0 -232
- package/.agent/skills/gstack-browse/src/cookie-picker-routes.ts +0 -343
- package/.agent/skills/gstack-browse/src/cookie-picker-ui.ts +0 -717
- package/.agent/skills/gstack-browse/src/domain-skill-commands.ts +0 -301
- package/.agent/skills/gstack-browse/src/domain-skills.ts +0 -438
- package/.agent/skills/gstack-browse/src/error-handling.ts +0 -2
- package/.agent/skills/gstack-browse/src/extension-id.ts +0 -25
- package/.agent/skills/gstack-browse/src/file-permissions.ts +0 -370
- package/.agent/skills/gstack-browse/src/find-browse.ts +0 -118
- package/.agent/skills/gstack-browse/src/find-security-sidecar.ts +0 -78
- package/.agent/skills/gstack-browse/src/media-extract.ts +0 -177
- package/.agent/skills/gstack-browse/src/memory-command.ts +0 -115
- package/.agent/skills/gstack-browse/src/memory-snapshot.ts +0 -73
- package/.agent/skills/gstack-browse/src/meta-commands.ts +0 -1150
- package/.agent/skills/gstack-browse/src/network-capture.ts +0 -179
- package/.agent/skills/gstack-browse/src/path-security.ts +0 -132
- package/.agent/skills/gstack-browse/src/platform.ts +0 -38
- package/.agent/skills/gstack-browse/src/port-allocator.ts +0 -140
- package/.agent/skills/gstack-browse/src/project-slug.ts +0 -36
- package/.agent/skills/gstack-browse/src/proxy-config.ts +0 -283
- package/.agent/skills/gstack-browse/src/proxy-redact.ts +0 -46
- package/.agent/skills/gstack-browse/src/pty-session-cookie.ts +0 -64
- package/.agent/skills/gstack-browse/src/pty-session-lease.ts +0 -137
- package/.agent/skills/gstack-browse/src/read-commands.ts +0 -756
- package/.agent/skills/gstack-browse/src/routes/activity.ts +0 -63
- package/.agent/skills/gstack-browse/src/routes/commands.ts +0 -154
- package/.agent/skills/gstack-browse/src/routes/core.ts +0 -123
- package/.agent/skills/gstack-browse/src/routes/files.ts +0 -47
- package/.agent/skills/gstack-browse/src/routes/index.ts +0 -27
- package/.agent/skills/gstack-browse/src/routes/inspector.ts +0 -132
- package/.agent/skills/gstack-browse/src/routes/pairing.ts +0 -158
- package/.agent/skills/gstack-browse/src/routes/pty.ts +0 -254
- package/.agent/skills/gstack-browse/src/routes/table.ts +0 -182
- package/.agent/skills/gstack-browse/src/routes/tokens.ts +0 -86
- package/.agent/skills/gstack-browse/src/routes/tunnel.ts +0 -62
- package/.agent/skills/gstack-browse/src/sanitize.ts +0 -49
- package/.agent/skills/gstack-browse/src/screenshot-size-guard.ts +0 -106
- package/.agent/skills/gstack-browse/src/security-classifier.ts +0 -258
- package/.agent/skills/gstack-browse/src/security-sidecar-client.ts +0 -234
- package/.agent/skills/gstack-browse/src/security-sidecar-entry.ts +0 -120
- package/.agent/skills/gstack-browse/src/security.ts +0 -328
- package/.agent/skills/gstack-browse/src/server.ts +0 -2220
- package/.agent/skills/gstack-browse/src/session-cookie-store.ts +0 -124
- package/.agent/skills/gstack-browse/src/session-persist.ts +0 -178
- package/.agent/skills/gstack-browse/src/skill-token.ts +0 -91
- package/.agent/skills/gstack-browse/src/snapshot-flags.ts +0 -30
- package/.agent/skills/gstack-browse/src/snapshot.ts +0 -656
- package/.agent/skills/gstack-browse/src/socks-bridge.ts +0 -314
- package/.agent/skills/gstack-browse/src/sse-helpers.ts +0 -147
- package/.agent/skills/gstack-browse/src/sse-session-cookie.ts +0 -65
- package/.agent/skills/gstack-browse/src/stealth.ts +0 -574
- package/.agent/skills/gstack-browse/src/tab-session.ts +0 -210
- package/.agent/skills/gstack-browse/src/telemetry.ts +0 -115
- package/.agent/skills/gstack-browse/src/terminal-agent-control.ts +0 -379
- package/.agent/skills/gstack-browse/src/terminal-agent.ts +0 -1124
- package/.agent/skills/gstack-browse/src/terminal-pty-lifecycle.ts +0 -144
- package/.agent/skills/gstack-browse/src/token-registry.ts +0 -721
- package/.agent/skills/gstack-browse/src/tunnel-denial-log.ts +0 -97
- package/.agent/skills/gstack-browse/src/url-validation.ts +0 -299
- package/.agent/skills/gstack-browse/src/welcome.html +0 -237
- package/.agent/skills/gstack-browse/src/write-commands.ts +0 -1438
- package/.agent/skills/gstack-browse/src/xprotect-heal.ts +0 -439
- package/.agent/skills/gstack-browse/src/xvfb.ts +0 -313
- package/.agent/skills/gstack-browse/test/activity.test.ts +0 -120
- package/.agent/skills/gstack-browse/test/adversarial-security.test.ts +0 -32
- package/.agent/skills/gstack-browse/test/batch.test.ts +0 -267
- package/.agent/skills/gstack-browse/test/bridge-chromium-e2e.test.ts +0 -205
- package/.agent/skills/gstack-browse/test/browse-client.test.ts +0 -300
- package/.agent/skills/gstack-browse/test/browse-eval-wrapping.test.ts +0 -86
- package/.agent/skills/gstack-browse/test/browser-manager-custom-chromium.test.ts +0 -67
- package/.agent/skills/gstack-browse/test/browser-manager-unit.test.ts +0 -335
- package/.agent/skills/gstack-browse/test/browser-skill-commands.test.ts +0 -433
- package/.agent/skills/gstack-browse/test/browser-skill-write.test.ts +0 -350
- package/.agent/skills/gstack-browse/test/browser-skills-e2e.test.ts +0 -101
- package/.agent/skills/gstack-browse/test/browser-skills-storage.test.ts +0 -283
- package/.agent/skills/gstack-browse/test/build-command-response.test.ts +0 -68
- package/.agent/skills/gstack-browse/test/build.test.ts +0 -52
- package/.agent/skills/gstack-browse/test/bun-child-config.test.ts +0 -83
- package/.agent/skills/gstack-browse/test/bun-polyfill.test.ts +0 -402
- package/.agent/skills/gstack-browse/test/busy-daemon-iron-rule.test.ts +0 -270
- package/.agent/skills/gstack-browse/test/busy-daemon-recovery.test.ts +0 -134
- package/.agent/skills/gstack-browse/test/cdp-allowlist.test.ts +0 -109
- package/.agent/skills/gstack-browse/test/cdp-e2e.test.ts +0 -117
- package/.agent/skills/gstack-browse/test/cdp-inspector-history-cap.test.ts +0 -95
- package/.agent/skills/gstack-browse/test/cdp-mutex.test.ts +0 -113
- package/.agent/skills/gstack-browse/test/cdp-session-cleanup.test.ts +0 -171
- package/.agent/skills/gstack-browse/test/chromium-profile-isolation.test.ts +0 -418
- package/.agent/skills/gstack-browse/test/claude-bin.test.ts +0 -95
- package/.agent/skills/gstack-browse/test/cli-chain-stdin.test.ts +0 -122
- package/.agent/skills/gstack-browse/test/cli-config-mismatch-message.test.ts +0 -72
- package/.agent/skills/gstack-browse/test/cli-lock.test.ts +0 -120
- package/.agent/skills/gstack-browse/test/cli-loopback-no-proxy.test.ts +0 -77
- package/.agent/skills/gstack-browse/test/cli-setsid-daemonize.test.ts +0 -75
- package/.agent/skills/gstack-browse/test/cli-start-final-healthcheck.test.ts +0 -77
- package/.agent/skills/gstack-browse/test/cli-supervisor.test.ts +0 -194
- package/.agent/skills/gstack-browse/test/commands.test.ts +0 -2779
- package/.agent/skills/gstack-browse/test/compare-board.test.ts +0 -382
- package/.agent/skills/gstack-browse/test/config.test.ts +0 -519
- package/.agent/skills/gstack-browse/test/content-security.test.ts +0 -681
- package/.agent/skills/gstack-browse/test/cookie-auth-verification.test.ts +0 -760
- package/.agent/skills/gstack-browse/test/cookie-credential-deadline.test.ts +0 -225
- package/.agent/skills/gstack-browse/test/cookie-database.test.ts +0 -277
- package/.agent/skills/gstack-browse/test/cookie-fixture-delete-lease.test.ts +0 -256
- package/.agent/skills/gstack-browse/test/cookie-import-browser.test.ts +0 -523
- package/.agent/skills/gstack-browse/test/cookie-import-native-job.test.ts +0 -1645
- package/.agent/skills/gstack-browse/test/cookie-import-native-qualification.ts +0 -128
- package/.agent/skills/gstack-browse/test/cookie-import-native.test.ts +0 -436
- package/.agent/skills/gstack-browse/test/cookie-import-node.test.ts +0 -166
- package/.agent/skills/gstack-browse/test/cookie-import-operation.test.ts +0 -452
- package/.agent/skills/gstack-browse/test/cookie-import-reliability.test.ts +0 -659
- package/.agent/skills/gstack-browse/test/cookie-import-transport.test.ts +0 -38
- package/.agent/skills/gstack-browse/test/cookie-picker-binding-browser.test.ts +0 -96
- package/.agent/skills/gstack-browse/test/cookie-picker-binding.test.ts +0 -155
- package/.agent/skills/gstack-browse/test/cookie-picker-csrf-browser.test.ts +0 -50
- package/.agent/skills/gstack-browse/test/cookie-picker-routes.test.ts +0 -460
- package/.agent/skills/gstack-browse/test/cookie-picker-ui.test.ts +0 -535
- package/.agent/skills/gstack-browse/test/daemon-log-hygiene.test.ts +0 -135
- package/.agent/skills/gstack-browse/test/daemon-mismatch-refuse.test.ts +0 -178
- package/.agent/skills/gstack-browse/test/data-platform.test.ts +0 -176
- package/.agent/skills/gstack-browse/test/dia-gui-readiness.test.ts +0 -174
- package/.agent/skills/gstack-browse/test/dia-launch-comparison.test.ts +0 -180
- package/.agent/skills/gstack-browse/test/dia-macos-qualification.test.ts +0 -1695
- package/.agent/skills/gstack-browse/test/domain-skills-e2e.test.ts +0 -142
- package/.agent/skills/gstack-browse/test/domain-skills-storage.test.ts +0 -263
- package/.agent/skills/gstack-browse/test/dotenv-isolation.test.ts +0 -121
- package/.agent/skills/gstack-browse/test/dual-listener.test.ts +0 -411
- package/.agent/skills/gstack-browse/test/dx-polish.test.ts +0 -101
- package/.agent/skills/gstack-browse/test/error-handling.test.ts +0 -47
- package/.agent/skills/gstack-browse/test/extension-sender-auth.test.ts +0 -285
- package/.agent/skills/gstack-browse/test/extension-token.test.ts +0 -232
- package/.agent/skills/gstack-browse/test/file-drop.test.ts +0 -271
- package/.agent/skills/gstack-browse/test/file-permissions.test.ts +0 -346
- package/.agent/skills/gstack-browse/test/fill-change-event.test.ts +0 -78
- package/.agent/skills/gstack-browse/test/find-browse.test.ts +0 -61
- package/.agent/skills/gstack-browse/test/findport.test.ts +0 -232
- package/.agent/skills/gstack-browse/test/fixtures/basic.html +0 -33
- package/.agent/skills/gstack-browse/test/fixtures/change-only-validator.html +0 -31
- package/.agent/skills/gstack-browse/test/fixtures/cursor-interactive.html +0 -22
- package/.agent/skills/gstack-browse/test/fixtures/dialog.html +0 -15
- package/.agent/skills/gstack-browse/test/fixtures/dropdown.html +0 -61
- package/.agent/skills/gstack-browse/test/fixtures/empty.html +0 -2
- package/.agent/skills/gstack-browse/test/fixtures/forms.html +0 -55
- package/.agent/skills/gstack-browse/test/fixtures/iframe.html +0 -30
- package/.agent/skills/gstack-browse/test/fixtures/injection-combined.html +0 -58
- package/.agent/skills/gstack-browse/test/fixtures/injection-hidden.html +0 -62
- package/.agent/skills/gstack-browse/test/fixtures/injection-social.html +0 -25
- package/.agent/skills/gstack-browse/test/fixtures/injection-visible.html +0 -26
- package/.agent/skills/gstack-browse/test/fixtures/media-page.html +0 -67
- package/.agent/skills/gstack-browse/test/fixtures/mock-claude/claude +0 -185
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-delete-lease.ts +0 -125
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-file-owners.ts +0 -132
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-launch.cjs +0 -139
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-process-observer.ts +0 -150
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-process.cjs +0 -13
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-remove-fixture.cjs +0 -42
- package/.agent/skills/gstack-browse/test/fixtures/network-idle.html +0 -30
- package/.agent/skills/gstack-browse/test/fixtures/qa-eval-checkout.html +0 -108
- package/.agent/skills/gstack-browse/test/fixtures/qa-eval-spa.html +0 -98
- package/.agent/skills/gstack-browse/test/fixtures/qa-eval.html +0 -51
- package/.agent/skills/gstack-browse/test/fixtures/qa-only.html +0 -13
- package/.agent/skills/gstack-browse/test/fixtures/responsive.html +0 -49
- package/.agent/skills/gstack-browse/test/fixtures/route-dispatch-allowlist.json +0 -22
- package/.agent/skills/gstack-browse/test/fixtures/snapshot-ambiguous.html +0 -24
- package/.agent/skills/gstack-browse/test/fixtures/snapshot.html +0 -55
- package/.agent/skills/gstack-browse/test/fixtures/spa.html +0 -24
- package/.agent/skills/gstack-browse/test/fixtures/states.html +0 -17
- package/.agent/skills/gstack-browse/test/fixtures/upload-path-validation.ts +0 -91
- package/.agent/skills/gstack-browse/test/fixtures/upload.html +0 -25
- package/.agent/skills/gstack-browse/test/from-file-path-validation.test.ts +0 -68
- package/.agent/skills/gstack-browse/test/gstack-config.test.ts +0 -252
- package/.agent/skills/gstack-browse/test/gstack-update-check.test.ts +0 -614
- package/.agent/skills/gstack-browse/test/handoff.test.ts +0 -508
- package/.agent/skills/gstack-browse/test/headless-gpu-and-reap.test.ts +0 -286
- package/.agent/skills/gstack-browse/test/headless-launch-options.test.ts +0 -93
- package/.agent/skills/gstack-browse/test/launch-signal-flags.test.ts +0 -75
- package/.agent/skills/gstack-browse/test/learnings-injection.test.ts +0 -33
- package/.agent/skills/gstack-browse/test/media-extract-unit.test.ts +0 -294
- package/.agent/skills/gstack-browse/test/memory-command.test.ts +0 -219
- package/.agent/skills/gstack-browse/test/memory-leak-reproducer.test.ts +0 -133
- package/.agent/skills/gstack-browse/test/navigation-guard.test.ts +0 -106
- package/.agent/skills/gstack-browse/test/output-path-symlinks.test.ts +0 -76
- package/.agent/skills/gstack-browse/test/pair-agent-e2e.test.ts +0 -793
- package/.agent/skills/gstack-browse/test/pair-agent-optin-gate.test.ts +0 -171
- package/.agent/skills/gstack-browse/test/pair-agent-tunnel-eval.test.ts +0 -501
- package/.agent/skills/gstack-browse/test/path-security-realpath.test.ts +0 -89
- package/.agent/skills/gstack-browse/test/path-validation.test.ts +0 -268
- package/.agent/skills/gstack-browse/test/pdf-flags.test.ts +0 -86
- package/.agent/skills/gstack-browse/test/platform.test.ts +0 -37
- package/.agent/skills/gstack-browse/test/playwright-core-patch.test.ts +0 -62
- package/.agent/skills/gstack-browse/test/poisoned-bundle-probe.test.ts +0 -148
- package/.agent/skills/gstack-browse/test/process-liveness-windows.test.ts +0 -176
- package/.agent/skills/gstack-browse/test/proxy-config.test.ts +0 -189
- package/.agent/skills/gstack-browse/test/proxy-env-daemon.test.ts +0 -137
- package/.agent/skills/gstack-browse/test/proxy-env.test.ts +0 -189
- package/.agent/skills/gstack-browse/test/proxy-redact.test.ts +0 -64
- package/.agent/skills/gstack-browse/test/pty-inject-scan.test.ts +0 -153
- package/.agent/skills/gstack-browse/test/pty-session-lease.test.ts +0 -98
- package/.agent/skills/gstack-browse/test/rebrand-signed-bundle.test.ts +0 -53
- package/.agent/skills/gstack-browse/test/regression-pr1169-pdf-from-file-invalid-json.test.ts +0 -84
- package/.agent/skills/gstack-browse/test/restart-env.test.ts +0 -39
- package/.agent/skills/gstack-browse/test/route-test-harness.ts +0 -121
- package/.agent/skills/gstack-browse/test/runtime-root-server-bundle.test.ts +0 -100
- package/.agent/skills/gstack-browse/test/sanitize.test.ts +0 -112
- package/.agent/skills/gstack-browse/test/screenshot-size-guard.test.ts +0 -118
- package/.agent/skills/gstack-browse/test/security-adversarial-fixes.test.ts +0 -101
- package/.agent/skills/gstack-browse/test/security-adversarial.test.ts +0 -342
- package/.agent/skills/gstack-browse/test/security-audit-r2.test.ts +0 -432
- package/.agent/skills/gstack-browse/test/security-bench.test.ts +0 -159
- package/.agent/skills/gstack-browse/test/security-classifier-download-cleanup.test.ts +0 -138
- package/.agent/skills/gstack-browse/test/security-classifier.test.ts +0 -29
- package/.agent/skills/gstack-browse/test/security-integration.test.ts +0 -184
- package/.agent/skills/gstack-browse/test/security-live-playwright.test.ts +0 -195
- package/.agent/skills/gstack-browse/test/security-sidecar-client.test.ts +0 -66
- package/.agent/skills/gstack-browse/test/security.test.ts +0 -268
- package/.agent/skills/gstack-browse/test/server-auth.test.ts +0 -600
- package/.agent/skills/gstack-browse/test/server-embedder-terminal-port.test.ts +0 -301
- package/.agent/skills/gstack-browse/test/server-factory.test.ts +0 -640
- package/.agent/skills/gstack-browse/test/server-flush-trackers.test.ts +0 -73
- package/.agent/skills/gstack-browse/test/server-launch-resolution.test.ts +0 -49
- package/.agent/skills/gstack-browse/test/server-lock-errors.test.ts +0 -124
- package/.agent/skills/gstack-browse/test/server-no-import-side-effects.test.ts +0 -110
- package/.agent/skills/gstack-browse/test/server-proxy-fail-fast.test.ts +0 -98
- package/.agent/skills/gstack-browse/test/server-pty-lease-routes.test.ts +0 -156
- package/.agent/skills/gstack-browse/test/server-route-auth-blackbox.test.ts +0 -270
- package/.agent/skills/gstack-browse/test/server-route-dispatch-ratchet.test.ts +0 -129
- package/.agent/skills/gstack-browse/test/server-route-table.test.ts +0 -238
- package/.agent/skills/gstack-browse/test/server-sanitize-surrogates.test.ts +0 -182
- package/.agent/skills/gstack-browse/test/server-tmp-state-path.test.ts +0 -110
- package/.agent/skills/gstack-browse/test/session-cookie-store.test.ts +0 -159
- package/.agent/skills/gstack-browse/test/session-persist.test.ts +0 -287
- package/.agent/skills/gstack-browse/test/sidebar-tabs.test.ts +0 -268
- package/.agent/skills/gstack-browse/test/sidebar-ux.test.ts +0 -1006
- package/.agent/skills/gstack-browse/test/sidepanel-font-size.test.ts +0 -147
- package/.agent/skills/gstack-browse/test/sidepanel-patient-autoconnect.test.ts +0 -70
- package/.agent/skills/gstack-browse/test/sidepanel-reattach.test.ts +0 -93
- package/.agent/skills/gstack-browse/test/sidepanel-restart-dispose.test.ts +0 -106
- package/.agent/skills/gstack-browse/test/skill-token.test.ts +0 -167
- package/.agent/skills/gstack-browse/test/snapshot.test.ts +0 -593
- package/.agent/skills/gstack-browse/test/socks-bridge.test.ts +0 -461
- package/.agent/skills/gstack-browse/test/sse-helpers.test.ts +0 -194
- package/.agent/skills/gstack-browse/test/sse-session-cookie.test.ts +0 -155
- package/.agent/skills/gstack-browse/test/state-ttl.test.ts +0 -35
- package/.agent/skills/gstack-browse/test/stealth-extended.test.ts +0 -127
- package/.agent/skills/gstack-browse/test/stealth-layer-c.test.ts +0 -284
- package/.agent/skills/gstack-browse/test/stealth-webdriver.test.ts +0 -394
- package/.agent/skills/gstack-browse/test/stop-ack-before-shutdown.test.ts +0 -23
- package/.agent/skills/gstack-browse/test/stop-dead-daemon.test.ts +0 -153
- package/.agent/skills/gstack-browse/test/tab-each.test.ts +0 -196
- package/.agent/skills/gstack-browse/test/tab-guardrail.test.ts +0 -118
- package/.agent/skills/gstack-browse/test/tab-isolation.test.ts +0 -309
- package/.agent/skills/gstack-browse/test/tab-session-frame-detach.test.ts +0 -154
- package/.agent/skills/gstack-browse/test/telemetry-optout.test.ts +0 -171
- package/.agent/skills/gstack-browse/test/telemetry.test.ts +0 -77
- package/.agent/skills/gstack-browse/test/temp-dirs.test.ts +0 -129
- package/.agent/skills/gstack-browse/test/terminal-agent-detach-reattach.test.ts +0 -84
- package/.agent/skills/gstack-browse/test/terminal-agent-import.test.ts +0 -42
- package/.agent/skills/gstack-browse/test/terminal-agent-integration.test.ts +0 -565
- package/.agent/skills/gstack-browse/test/terminal-agent-keepalive.test.ts +0 -88
- package/.agent/skills/gstack-browse/test/terminal-agent-lifecycle.test.ts +0 -371
- package/.agent/skills/gstack-browse/test/terminal-agent-native-observation.test.ts +0 -66
- package/.agent/skills/gstack-browse/test/terminal-agent-owner-watchdog.test.ts +0 -81
- package/.agent/skills/gstack-browse/test/terminal-agent-pid-identity.test.ts +0 -161
- package/.agent/skills/gstack-browse/test/terminal-agent-port-range.test.ts +0 -80
- package/.agent/skills/gstack-browse/test/terminal-agent-publication-lock.test.ts +0 -289
- package/.agent/skills/gstack-browse/test/terminal-agent-ring-buffer-runtime.test.ts +0 -157
- package/.agent/skills/gstack-browse/test/terminal-agent-session-routing.test.ts +0 -96
- package/.agent/skills/gstack-browse/test/terminal-agent-watchdog.test.ts +0 -101
- package/.agent/skills/gstack-browse/test/terminal-agent.test.ts +0 -259
- package/.agent/skills/gstack-browse/test/terminal-pty-lifecycle.test.ts +0 -212
- package/.agent/skills/gstack-browse/test/test-server.ts +0 -57
- package/.agent/skills/gstack-browse/test/token-registry.test.ts +0 -615
- package/.agent/skills/gstack-browse/test/tunnel-gate-unit.test.ts +0 -129
- package/.agent/skills/gstack-browse/test/tunnel-revoke-cli.test.ts +0 -455
- package/.agent/skills/gstack-browse/test/url-validation.test.ts +0 -384
- package/.agent/skills/gstack-browse/test/watch.test.ts +0 -129
- package/.agent/skills/gstack-browse/test/watchdog.test.ts +0 -352
- package/.agent/skills/gstack-browse/test/welcome-page.test.ts +0 -143
- package/.agent/skills/gstack-browse/test/windows-spawn-hide.test.ts +0 -167
- package/.agent/skills/gstack-browse/test/xprotect-heal.test.ts +0 -457
- package/.agent/skills/gstack-browse/test/xvfb.test.ts +0 -603
- package/.agent/skills/gstack-canary/SKILL.md +0 -699
- package/.agent/skills/gstack-canary/SKILL.md.tmpl +0 -254
- package/.agent/skills/gstack-careful/SKILL.md +0 -96
- package/.agent/skills/gstack-careful/SKILL.md.tmpl +0 -107
- package/.agent/skills/gstack-careful/bin/check-careful.sh +0 -407
- package/.agent/skills/gstack-careful/bin/hook-extract.sh +0 -198
- package/.agent/skills/gstack-codex/SKILL.md +0 -889
- package/.agent/skills/gstack-codex/SKILL.md.tmpl +0 -358
- package/.agent/skills/gstack-codex/sections/challenge-mode.md +0 -150
- package/.agent/skills/gstack-codex/sections/challenge-mode.md.tmpl +0 -136
- package/.agent/skills/gstack-codex/sections/consult-mode.md +0 -221
- package/.agent/skills/gstack-codex/sections/consult-mode.md.tmpl +0 -207
- package/.agent/skills/gstack-codex/sections/manifest.json +0 -26
- package/.agent/skills/gstack-codex/sections/review-mode.md +0 -265
- package/.agent/skills/gstack-codex/sections/review-mode.md.tmpl +0 -246
- package/.agent/skills/gstack-context-restore/SKILL.md +0 -612
- package/.agent/skills/gstack-context-restore/SKILL.md.tmpl +0 -289
- package/.agent/skills/gstack-context-save/SKILL.md +0 -631
- package/.agent/skills/gstack-context-save/SKILL.md.tmpl +0 -307
- package/.agent/skills/gstack-cso/ACKNOWLEDGEMENTS.md +0 -17
- package/.agent/skills/gstack-cso/SKILL.md +0 -166
- package/.agent/skills/gstack-cso/SKILL.md.tmpl +0 -162
- package/.agent/skills/gstack-cso/sections/audit-phases.md +0 -116
- package/.agent/skills/gstack-cso/sections/audit-phases.md.tmpl +0 -114
- package/.agent/skills/gstack-cso/sections/manifest.json +0 -14
- package/.agent/skills/gstack-design-consultation/SKILL.md +0 -795
- package/.agent/skills/gstack-design-consultation/SKILL.md.tmpl +0 -234
- package/.agent/skills/gstack-design-consultation/sections/manifest.json +0 -14
- package/.agent/skills/gstack-design-consultation/sections/proposal-and-preview.md +0 -595
- package/.agent/skills/gstack-design-consultation/sections/proposal-and-preview.md.tmpl +0 -350
- package/.agent/skills/gstack-design-html/SKILL.md +0 -866
- package/.agent/skills/gstack-design-html/SKILL.md.tmpl +0 -476
- package/.agent/skills/gstack-design-html/sections/detector-install-offer.md +0 -40
- package/.agent/skills/gstack-design-html/sections/detector-install-offer.md.tmpl +0 -6
- package/.agent/skills/gstack-design-html/sections/doctrine.md +0 -86
- package/.agent/skills/gstack-design-html/sections/doctrine.md.tmpl +0 -1
- package/.agent/skills/gstack-design-html/sections/manifest.json +0 -26
- package/.agent/skills/gstack-design-html/sections/pretext-patterns.md +0 -145
- package/.agent/skills/gstack-design-html/sections/pretext-patterns.md.tmpl +0 -143
- package/.agent/skills/gstack-design-html/vendor/pretext.js +0 -5
- package/.agent/skills/gstack-design-review/SKILL.md +0 -1957
- package/.agent/skills/gstack-design-review/SKILL.md.tmpl +0 -344
- package/.agent/skills/gstack-design-shotgun/SKILL.md +0 -885
- package/.agent/skills/gstack-design-shotgun/SKILL.md.tmpl +0 -310
- package/.agent/skills/gstack-design-shotgun/sections/doctrine.md +0 -86
- package/.agent/skills/gstack-design-shotgun/sections/doctrine.md.tmpl +0 -1
- package/.agent/skills/gstack-design-shotgun/sections/manifest.json +0 -14
- package/.agent/skills/gstack-deslop-shared-libs/SKILL.md +0 -209
- package/.agent/skills/gstack-deslop-shared-libs/SKILL.md.tmpl +0 -181
- package/.agent/skills/gstack-devex-review/SKILL.md +0 -1107
- package/.agent/skills/gstack-devex-review/SKILL.md.tmpl +0 -241
- package/.agent/skills/gstack-diagram/SKILL.md +0 -295
- package/.agent/skills/gstack-diagram/SKILL.md.tmpl +0 -175
- package/.agent/skills/gstack-document-generate/SKILL.md +0 -814
- package/.agent/skills/gstack-document-generate/SKILL.md.tmpl +0 -460
- package/.agent/skills/gstack-document-release/SKILL.md +0 -557
- package/.agent/skills/gstack-document-release/SKILL.md.tmpl +0 -188
- package/.agent/skills/gstack-document-release/sections/audit-scope.md +0 -51
- package/.agent/skills/gstack-document-release/sections/audit-scope.md.tmpl +0 -49
- package/.agent/skills/gstack-document-release/sections/manifest.json +0 -20
- package/.agent/skills/gstack-document-release/sections/release-body.md +0 -640
- package/.agent/skills/gstack-document-release/sections/release-body.md.tmpl +0 -428
- package/.agent/skills/gstack-eval-plan/SKILL.md +0 -472
- package/.agent/skills/gstack-eval-plan/SKILL.md.tmpl +0 -157
- package/.agent/skills/gstack-freeze/SKILL.md +0 -79
- package/.agent/skills/gstack-freeze/SKILL.md.tmpl +0 -96
- package/.agent/skills/gstack-freeze/bin/check-freeze.sh +0 -210
- package/.agent/skills/gstack-freeze/bin/freeze-state.sh +0 -68
- package/.agent/skills/gstack-gstack-upgrade/SKILL.md +0 -410
- package/.agent/skills/gstack-gstack-upgrade/SKILL.md.tmpl +0 -413
- package/.agent/skills/gstack-gstack-upgrade/migrations/v0.15.2.0.sh +0 -20
- package/.agent/skills/gstack-gstack-upgrade/migrations/v0.16.2.0.sh +0 -56
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.0.0.0.sh +0 -40
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.1.3.0.sh +0 -137
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.17.0.0.sh +0 -59
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.27.0.0.sh +0 -432
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.37.0.0.sh +0 -104
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.38.1.0.sh +0 -104
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.40.0.0.sh +0 -144
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.58.0.0.sh +0 -65
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.65.0.0.sh +0 -152
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.67.0.0.sh +0 -73
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.78.0.0.sh +0 -30
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.86.0.0.sh +0 -12
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.91.19.0.sh +0 -53
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.91.65.0.sh +0 -31
- package/.agent/skills/gstack-guard/SKILL.md +0 -60
- package/.agent/skills/gstack-guard/SKILL.md.tmpl +0 -88
- package/.agent/skills/gstack-health/SKILL.md +0 -710
- package/.agent/skills/gstack-health/SKILL.md.tmpl +0 -386
- package/.agent/skills/gstack-investigate/SKILL.md +0 -706
- package/.agent/skills/gstack-investigate/SKILL.md.tmpl +0 -305
- package/.agent/skills/gstack-investigate/sections/backlog.md +0 -31
- package/.agent/skills/gstack-investigate/sections/backlog.md.tmpl +0 -29
- package/.agent/skills/gstack-investigate/sections/eval-bisect.md +0 -25
- package/.agent/skills/gstack-investigate/sections/eval-bisect.md.tmpl +0 -23
- package/.agent/skills/gstack-investigate/sections/flake.md +0 -50
- package/.agent/skills/gstack-investigate/sections/flake.md.tmpl +0 -43
- package/.agent/skills/gstack-investigate/sections/manifest.json +0 -38
- package/.agent/skills/gstack-investigate/sections/polluter.md +0 -33
- package/.agent/skills/gstack-investigate/sections/polluter.md.tmpl +0 -31
- package/.agent/skills/gstack-investigate/sections/race.md +0 -27
- package/.agent/skills/gstack-investigate/sections/race.md.tmpl +0 -25
- package/.agent/skills/gstack-ios-clean/SKILL.md +0 -427
- package/.agent/skills/gstack-ios-clean/SKILL.md.tmpl +0 -107
- package/.agent/skills/gstack-ios-design-review/SKILL.md +0 -434
- package/.agent/skills/gstack-ios-design-review/SKILL.md.tmpl +0 -111
- package/.agent/skills/gstack-ios-fix/SKILL.md +0 -423
- package/.agent/skills/gstack-ios-fix/SKILL.md.tmpl +0 -103
- package/.agent/skills/gstack-ios-qa/SKILL.md +0 -643
- package/.agent/skills/gstack-ios-qa/SKILL.md.tmpl +0 -297
- package/.agent/skills/gstack-ios-qa/daemon/src/allowlist.ts +0 -114
- package/.agent/skills/gstack-ios-qa/daemon/src/audit.ts +0 -97
- package/.agent/skills/gstack-ios-qa/daemon/src/auth-mint.ts +0 -89
- package/.agent/skills/gstack-ios-qa/daemon/src/cli-mint.ts +0 -149
- package/.agent/skills/gstack-ios-qa/daemon/src/devicectl.ts +0 -335
- package/.agent/skills/gstack-ios-qa/daemon/src/index.ts +0 -697
- package/.agent/skills/gstack-ios-qa/daemon/src/proxy.ts +0 -160
- package/.agent/skills/gstack-ios-qa/daemon/src/session-tokens.ts +0 -126
- package/.agent/skills/gstack-ios-qa/daemon/src/single-instance.ts +0 -171
- package/.agent/skills/gstack-ios-qa/daemon/src/tailscale-localapi.ts +0 -120
- package/.agent/skills/gstack-ios-qa/daemon/src/tunnel-bootstrap.ts +0 -459
- package/.agent/skills/gstack-ios-qa/daemon/src/types.ts +0 -91
- package/.agent/skills/gstack-ios-qa/daemon/test/allowlist.test.ts +0 -146
- package/.agent/skills/gstack-ios-qa/daemon/test/audit.test.ts +0 -111
- package/.agent/skills/gstack-ios-qa/daemon/test/auth-mint.test.ts +0 -106
- package/.agent/skills/gstack-ios-qa/daemon/test/cli-mint.test.ts +0 -120
- package/.agent/skills/gstack-ios-qa/daemon/test/daemon-integration.test.ts +0 -855
- package/.agent/skills/gstack-ios-qa/daemon/test/fake-device.ts +0 -238
- package/.agent/skills/gstack-ios-qa/daemon/test/proxy-classify.test.ts +0 -47
- package/.agent/skills/gstack-ios-qa/daemon/test/proxy-transport.test.ts +0 -499
- package/.agent/skills/gstack-ios-qa/daemon/test/session-tokens.test.ts +0 -156
- package/.agent/skills/gstack-ios-qa/daemon/test/single-instance.test.ts +0 -96
- package/.agent/skills/gstack-ios-qa/daemon/test/tailscale-localapi.test.ts +0 -55
- package/.agent/skills/gstack-ios-qa/daemon/test/tunnel-bootstrap.test.ts +0 -1030
- package/.agent/skills/gstack-ios-qa/templates/Bridges.swift.template +0 -474
- package/.agent/skills/gstack-ios-qa/templates/DebugBridgeManager.swift.template +0 -38
- package/.agent/skills/gstack-ios-qa/templates/DebugBridgeTouch.h.template +0 -38
- package/.agent/skills/gstack-ios-qa/templates/DebugBridgeTouch.m.template +0 -365
- package/.agent/skills/gstack-ios-qa/templates/DebugBridgeWiring.swift.template +0 -59
- package/.agent/skills/gstack-ios-qa/templates/DebugOverlay.swift.template +0 -137
- package/.agent/skills/gstack-ios-qa/templates/Package.swift.template +0 -82
- package/.agent/skills/gstack-ios-qa/templates/StateAccessor.swift.template +0 -41
- package/.agent/skills/gstack-ios-qa/templates/StateServer.swift.template +0 -625
- package/.agent/skills/gstack-ios-sync/SKILL.md +0 -418
- package/.agent/skills/gstack-ios-sync/SKILL.md.tmpl +0 -99
- package/.agent/skills/gstack-land-and-deploy/SKILL.md +0 -1028
- package/.agent/skills/gstack-land-and-deploy/SKILL.md.tmpl +0 -509
- package/.agent/skills/gstack-land-and-deploy/sections/first-run-validation.md +0 -188
- package/.agent/skills/gstack-land-and-deploy/sections/first-run-validation.md.tmpl +0 -150
- package/.agent/skills/gstack-land-and-deploy/sections/manifest.json +0 -26
- package/.agent/skills/gstack-land-and-deploy/sections/merge-and-deploy.md +0 -250
- package/.agent/skills/gstack-land-and-deploy/sections/merge-and-deploy.md.tmpl +0 -248
- package/.agent/skills/gstack-land-and-deploy/sections/readiness-gate.md +0 -308
- package/.agent/skills/gstack-land-and-deploy/sections/readiness-gate.md.tmpl +0 -306
- package/.agent/skills/gstack-landing-report/SKILL.md +0 -488
- package/.agent/skills/gstack-landing-report/SKILL.md.tmpl +0 -164
- package/.agent/skills/gstack-learn/SKILL.md +0 -519
- package/.agent/skills/gstack-learn/SKILL.md.tmpl +0 -197
- package/.agent/skills/gstack-make-pdf/SKILL.md +0 -409
- package/.agent/skills/gstack-make-pdf/SKILL.md.tmpl +0 -253
- package/.agent/skills/gstack-make-pdf/src/asideClient.ts +0 -114
- package/.agent/skills/gstack-make-pdf/src/cli.ts +0 -294
- package/.agent/skills/gstack-make-pdf/src/commands.ts +0 -62
- package/.agent/skills/gstack-make-pdf/src/diagram-prepass.ts +0 -834
- package/.agent/skills/gstack-make-pdf/src/html-to-docx.d.ts +0 -7
- package/.agent/skills/gstack-make-pdf/src/image-policy.ts +0 -236
- package/.agent/skills/gstack-make-pdf/src/image-size.ts +0 -117
- package/.agent/skills/gstack-make-pdf/src/orchestrator.ts +0 -339
- package/.agent/skills/gstack-make-pdf/src/pdftotext.ts +0 -318
- package/.agent/skills/gstack-make-pdf/src/print-css.ts +0 -452
- package/.agent/skills/gstack-make-pdf/src/render.ts +0 -636
- package/.agent/skills/gstack-make-pdf/src/setup.ts +0 -113
- package/.agent/skills/gstack-make-pdf/src/smartypants.ts +0 -105
- package/.agent/skills/gstack-make-pdf/src/toc-pages.ts +0 -197
- package/.agent/skills/gstack-make-pdf/src/types.ts +0 -109
- package/.agent/skills/gstack-make-pdf/test/asideClient.test.ts +0 -153
- package/.agent/skills/gstack-make-pdf/test/cli-args.test.ts +0 -94
- package/.agent/skills/gstack-make-pdf/test/cli-exit-codes.test.ts +0 -329
- package/.agent/skills/gstack-make-pdf/test/diagram-prepass.test.ts +0 -743
- package/.agent/skills/gstack-make-pdf/test/e2e/ci-prereqs.test.ts +0 -48
- package/.agent/skills/gstack-make-pdf/test/e2e/combined-gate.test.ts +0 -74
- package/.agent/skills/gstack-make-pdf/test/e2e/diagram-gate.test.ts +0 -173
- package/.agent/skills/gstack-make-pdf/test/e2e/emoji-gate.test.ts +0 -191
- package/.agent/skills/gstack-make-pdf/test/e2e/format-gate.test.ts +0 -130
- package/.agent/skills/gstack-make-pdf/test/e2e/landscape-gate.test.ts +0 -150
- package/.agent/skills/gstack-make-pdf/test/e2e/toc-gate.test.ts +0 -143
- package/.agent/skills/gstack-make-pdf/test/fixtures/combined-gate.expected.txt +0 -20
- package/.agent/skills/gstack-make-pdf/test/fixtures/combined-gate.md +0 -30
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/huge-noise.png +0 -0
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/red-box.png +0 -0
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/wide-arch.png +0 -0
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/wide-screenshot.png +0 -0
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-gate.md +0 -48
- package/.agent/skills/gstack-make-pdf/test/fixtures/emoji-gate.md +0 -12
- package/.agent/skills/gstack-make-pdf/test/fixtures/landscape-gate.md +0 -52
- package/.agent/skills/gstack-make-pdf/test/image-policy.test.ts +0 -215
- package/.agent/skills/gstack-make-pdf/test/pdftotext.test.ts +0 -327
- package/.agent/skills/gstack-make-pdf/test/render-offline-sanitize.test.ts +0 -268
- package/.agent/skills/gstack-make-pdf/test/render.test.ts +0 -603
- package/.agent/skills/gstack-make-pdf/test/setup-smoke.test.ts +0 -259
- package/.agent/skills/gstack-make-pdf/test/toc-anchors.test.ts +0 -95
- package/.agent/skills/gstack-make-pdf/test/toc-pages.test.ts +0 -148
- package/.agent/skills/gstack-office-hours/SKILL.md +0 -1324
- package/.agent/skills/gstack-office-hours/SKILL.md.tmpl +0 -372
- package/.agent/skills/gstack-office-hours/sections/design-and-handoff.md +0 -706
- package/.agent/skills/gstack-office-hours/sections/design-and-handoff.md.tmpl +0 -547
- package/.agent/skills/gstack-office-hours/sections/manifest.json +0 -26
- package/.agent/skills/gstack-office-hours/sections/phase-2a-startup-diagnostic.md +0 -163
- package/.agent/skills/gstack-office-hours/sections/phase-2a-startup-diagnostic.md.tmpl +0 -161
- package/.agent/skills/gstack-office-hours/sections/phase-2b-builder-brainstorm.md +0 -43
- package/.agent/skills/gstack-office-hours/sections/phase-2b-builder-brainstorm.md.tmpl +0 -41
- package/.agent/skills/gstack-open-gstack-browser/SKILL.md +0 -414
- package/.agent/skills/gstack-open-gstack-browser/SKILL.md.tmpl +0 -248
- package/.agent/skills/gstack-pair-agent/SKILL.md +0 -730
- package/.agent/skills/gstack-pair-agent/SKILL.md.tmpl +0 -366
- package/.agent/skills/gstack-plan-ceo-review/SKILL.md +0 -1245
- package/.agent/skills/gstack-plan-ceo-review/SKILL.md.tmpl +0 -596
- package/.agent/skills/gstack-plan-ceo-review/bin/mode-handoff-hook +0 -5
- package/.agent/skills/gstack-plan-ceo-review/bin/mode-handoff-hook.ts +0 -34
- package/.agent/skills/gstack-plan-ceo-review/sections/checklist.md +0 -37
- package/.agent/skills/gstack-plan-ceo-review/sections/checklist.md.tmpl +0 -35
- package/.agent/skills/gstack-plan-ceo-review/sections/manifest.json +0 -23
- package/.agent/skills/gstack-plan-ceo-review/sections/review-sections.md +0 -1207
- package/.agent/skills/gstack-plan-ceo-review/sections/review-sections.md.tmpl +0 -605
- package/.agent/skills/gstack-plan-design-review/SKILL.md +0 -1230
- package/.agent/skills/gstack-plan-design-review/SKILL.md.tmpl +0 -344
- package/.agent/skills/gstack-plan-design-review/sections/checklist.md +0 -27
- package/.agent/skills/gstack-plan-design-review/sections/checklist.md.tmpl +0 -25
- package/.agent/skills/gstack-plan-design-review/sections/manifest.json +0 -23
- package/.agent/skills/gstack-plan-design-review/sections/review-sections.md +0 -727
- package/.agent/skills/gstack-plan-design-review/sections/review-sections.md.tmpl +0 -281
- package/.agent/skills/gstack-plan-devex-review/SKILL.md +0 -1072
- package/.agent/skills/gstack-plan-devex-review/SKILL.md.tmpl +0 -459
- package/.agent/skills/gstack-plan-devex-review/dx-hall-of-fame.md +0 -127
- package/.agent/skills/gstack-plan-devex-review/sections/checklist.md +0 -33
- package/.agent/skills/gstack-plan-devex-review/sections/checklist.md.tmpl +0 -31
- package/.agent/skills/gstack-plan-devex-review/sections/manifest.json +0 -23
- package/.agent/skills/gstack-plan-devex-review/sections/review-sections.md +0 -1037
- package/.agent/skills/gstack-plan-devex-review/sections/review-sections.md.tmpl +0 -446
- package/.agent/skills/gstack-plan-eng-review/SKILL.md +0 -702
- package/.agent/skills/gstack-plan-eng-review/SKILL.md.tmpl +0 -191
- package/.agent/skills/gstack-plan-eng-review/sections/checklist.md +0 -34
- package/.agent/skills/gstack-plan-eng-review/sections/checklist.md.tmpl +0 -32
- package/.agent/skills/gstack-plan-eng-review/sections/manifest.json +0 -23
- package/.agent/skills/gstack-plan-eng-review/sections/review-sections.md +0 -1545
- package/.agent/skills/gstack-plan-eng-review/sections/review-sections.md.tmpl +0 -614
- package/.agent/skills/gstack-plan-tune/SKILL.md +0 -986
- package/.agent/skills/gstack-plan-tune/SKILL.md.tmpl +0 -669
- package/.agent/skills/gstack-qa/SKILL.md +0 -781
- package/.agent/skills/gstack-qa/SKILL.md.tmpl +0 -305
- package/.agent/skills/gstack-qa/references/issue-taxonomy.md +0 -85
- package/.agent/skills/gstack-qa/sections/browser-setup.md +0 -114
- package/.agent/skills/gstack-qa/sections/browser-setup.md.tmpl +0 -28
- package/.agent/skills/gstack-qa/sections/browser-verify.md +0 -19
- package/.agent/skills/gstack-qa/sections/browser-verify.md.tmpl +0 -17
- package/.agent/skills/gstack-qa/sections/exploratory.md +0 -92
- package/.agent/skills/gstack-qa/sections/exploratory.md.tmpl +0 -1
- package/.agent/skills/gstack-qa/sections/manifest.json +0 -50
- package/.agent/skills/gstack-qa/sections/qa-patterns.md +0 -226
- package/.agent/skills/gstack-qa/sections/qa-patterns.md.tmpl +0 -1
- package/.agent/skills/gstack-qa/sections/scope.md +0 -23
- package/.agent/skills/gstack-qa/sections/scope.md.tmpl +0 -1
- package/.agent/skills/gstack-qa/sections/system-functional.md +0 -61
- package/.agent/skills/gstack-qa/sections/system-functional.md.tmpl +0 -1
- package/.agent/skills/gstack-qa/sections/test-bootstrap.md +0 -86
- package/.agent/skills/gstack-qa/sections/test-bootstrap.md.tmpl +0 -74
- package/.agent/skills/gstack-qa/templates/functional-report-template.md +0 -55
- package/.agent/skills/gstack-qa/templates/qa-report-template.md +0 -124
- package/.agent/skills/gstack-qa-only/SKILL.md +0 -595
- package/.agent/skills/gstack-qa-only/SKILL.md.tmpl +0 -214
- package/.agent/skills/gstack-qa-only/sections/exploratory.md +0 -107
- package/.agent/skills/gstack-qa-only/sections/exploratory.md.tmpl +0 -1
- package/.agent/skills/gstack-qa-only/sections/manifest.json +0 -19
- package/.agent/skills/gstack-qa-only/sections/reporting.md +0 -78
- package/.agent/skills/gstack-qa-only/sections/reporting.md.tmpl +0 -76
- package/.agent/skills/gstack-retro/SKILL.md +0 -1178
- package/.agent/skills/gstack-retro/SKILL.md.tmpl +0 -769
- package/.agent/skills/gstack-retro/sections/manifest.json +0 -14
- package/.agent/skills/gstack-retro/sections/report-format.md +0 -127
- package/.agent/skills/gstack-retro/sections/report-format.md.tmpl +0 -125
- package/.agent/skills/gstack-review/SKILL.md +0 -1199
- package/.agent/skills/gstack-review/SKILL.md.tmpl +0 -482
- package/.agent/skills/gstack-review/TODOS-format.md +0 -61
- package/.agent/skills/gstack-review/checklist.md +0 -222
- package/.agent/skills/gstack-review/design-checklist.md +0 -193
- package/.agent/skills/gstack-review/gate-integrity.md +0 -171
- package/.agent/skills/gstack-review/greptile-triage.md +0 -261
- package/.agent/skills/gstack-review/sections/adversarial.md +0 -296
- package/.agent/skills/gstack-review/sections/adversarial.md.tmpl +0 -1
- package/.agent/skills/gstack-review/sections/manifest.json +0 -32
- package/.agent/skills/gstack-review/sections/plan-completion.md +0 -232
- package/.agent/skills/gstack-review/sections/plan-completion.md.tmpl +0 -3
- package/.agent/skills/gstack-review/sections/review-army.md +0 -263
- package/.agent/skills/gstack-review/sections/review-army.md.tmpl +0 -1
- package/.agent/skills/gstack-review/sections/shared-code-reuse.md +0 -34
- package/.agent/skills/gstack-review/sections/shared-code-reuse.md.tmpl +0 -1
- package/.agent/skills/gstack-review/specialists/api-contract.md +0 -49
- package/.agent/skills/gstack-review/specialists/data-migration.md +0 -48
- package/.agent/skills/gstack-review/specialists/maintainability.md +0 -46
- package/.agent/skills/gstack-review/specialists/performance.md +0 -52
- package/.agent/skills/gstack-review/specialists/red-team.md +0 -45
- package/.agent/skills/gstack-review/specialists/security.md +0 -65
- package/.agent/skills/gstack-review/specialists/simplification.md +0 -49
- package/.agent/skills/gstack-review/specialists/testing.md +0 -160
- package/.agent/skills/gstack-scrape/SKILL.md +0 -431
- package/.agent/skills/gstack-scrape/SKILL.md.tmpl +0 -187
- package/.agent/skills/gstack-setup-browser-cookies/SKILL.md +0 -231
- package/.agent/skills/gstack-setup-browser-cookies/SKILL.md.tmpl +0 -64
- package/.agent/skills/gstack-setup-deploy/SKILL.md +0 -605
- package/.agent/skills/gstack-setup-deploy/SKILL.md.tmpl +0 -250
- package/.agent/skills/gstack-setup-gbrain/SKILL.md +0 -1074
- package/.agent/skills/gstack-setup-gbrain/SKILL.md.tmpl +0 -742
- package/.agent/skills/gstack-setup-gbrain/memory.md +0 -426
- package/.agent/skills/gstack-setup-gbrain/sections/brain-init.md +0 -269
- package/.agent/skills/gstack-setup-gbrain/sections/brain-init.md.tmpl +0 -267
- package/.agent/skills/gstack-setup-gbrain/sections/claude-md-persist.md +0 -40
- package/.agent/skills/gstack-setup-gbrain/sections/claude-md-persist.md.tmpl +0 -38
- package/.agent/skills/gstack-setup-gbrain/sections/engine-remediation.md +0 -71
- package/.agent/skills/gstack-setup-gbrain/sections/engine-remediation.md.tmpl +0 -69
- package/.agent/skills/gstack-setup-gbrain/sections/manifest.json +0 -32
- package/.agent/skills/gstack-setup-gbrain/sections/transcript-gate.md +0 -105
- package/.agent/skills/gstack-setup-gbrain/sections/transcript-gate.md.tmpl +0 -103
- package/.agent/skills/gstack-ship/SKILL.md +0 -1220
- package/.agent/skills/gstack-ship/SKILL.md.tmpl +0 -694
- package/.agent/skills/gstack-ship/sections/adversarial.md +0 -362
- package/.agent/skills/gstack-ship/sections/adversarial.md.tmpl +0 -22
- package/.agent/skills/gstack-ship/sections/apple-release.md +0 -98
- package/.agent/skills/gstack-ship/sections/apple-release.md.tmpl +0 -96
- package/.agent/skills/gstack-ship/sections/changelog.md +0 -47
- package/.agent/skills/gstack-ship/sections/changelog.md.tmpl +0 -5
- package/.agent/skills/gstack-ship/sections/documentation.md +0 -118
- package/.agent/skills/gstack-ship/sections/documentation.md.tmpl +0 -116
- package/.agent/skills/gstack-ship/sections/greptile-early.md +0 -57
- package/.agent/skills/gstack-ship/sections/greptile-early.md.tmpl +0 -48
- package/.agent/skills/gstack-ship/sections/greptile.md +0 -95
- package/.agent/skills/gstack-ship/sections/greptile.md.tmpl +0 -93
- package/.agent/skills/gstack-ship/sections/handoff.md +0 -22
- package/.agent/skills/gstack-ship/sections/handoff.md.tmpl +0 -20
- package/.agent/skills/gstack-ship/sections/manifest.json +0 -92
- package/.agent/skills/gstack-ship/sections/measure.md +0 -66
- package/.agent/skills/gstack-ship/sections/measure.md.tmpl +0 -64
- package/.agent/skills/gstack-ship/sections/plan-completion.md +0 -387
- package/.agent/skills/gstack-ship/sections/plan-completion.md.tmpl +0 -67
- package/.agent/skills/gstack-ship/sections/pr-body.md +0 -258
- package/.agent/skills/gstack-ship/sections/pr-body.md.tmpl +0 -241
- package/.agent/skills/gstack-ship/sections/review-army.md +0 -728
- package/.agent/skills/gstack-ship/sections/review-army.md.tmpl +0 -171
- package/.agent/skills/gstack-ship/sections/shared-code-reuse.md +0 -34
- package/.agent/skills/gstack-ship/sections/shared-code-reuse.md.tmpl +0 -1
- package/.agent/skills/gstack-ship/sections/test-coverage.md +0 -478
- package/.agent/skills/gstack-ship/sections/test-coverage.md.tmpl +0 -118
- package/.agent/skills/gstack-ship/sections/tests.md +0 -453
- package/.agent/skills/gstack-ship/sections/tests.md.tmpl +0 -144
- package/.agent/skills/gstack-skillify/SKILL.md +0 -790
- package/.agent/skills/gstack-skillify/SKILL.md.tmpl +0 -437
- package/.agent/skills/gstack-spec/SKILL.md +0 -893
- package/.agent/skills/gstack-spec/SKILL.md.tmpl +0 -498
- package/.agent/skills/gstack-spec/sections/gate-and-file.md +0 -470
- package/.agent/skills/gstack-spec/sections/gate-and-file.md.tmpl +0 -319
- package/.agent/skills/gstack-spec/sections/manifest.json +0 -14
- package/.agent/skills/gstack-sync-gbrain/SKILL.md +0 -935
- package/.agent/skills/gstack-sync-gbrain/SKILL.md.tmpl +0 -615
- package/.agent/skills/gstack-test-audit/SKILL.md +0 -500
- package/.agent/skills/gstack-test-audit/SKILL.md.tmpl +0 -149
- package/.agent/skills/gstack-unfreeze/SKILL.md +0 -39
- package/.agent/skills/gstack-unfreeze/SKILL.md.tmpl +0 -39
- package/.agent/skills/gstack-upgrade/SKILL.md +0 -410
- package/.agent/skills/gstack-upgrade/SKILL.md.tmpl +0 -413
- package/.agent/skills/gstack-upgrade/migrations/v0.15.2.0.sh +0 -20
- package/.agent/skills/gstack-upgrade/migrations/v0.16.2.0.sh +0 -56
- package/.agent/skills/gstack-upgrade/migrations/v1.0.0.0.sh +0 -40
- package/.agent/skills/gstack-upgrade/migrations/v1.1.3.0.sh +0 -137
- package/.agent/skills/gstack-upgrade/migrations/v1.17.0.0.sh +0 -59
- package/.agent/skills/gstack-upgrade/migrations/v1.27.0.0.sh +0 -432
- package/.agent/skills/gstack-upgrade/migrations/v1.37.0.0.sh +0 -104
- package/.agent/skills/gstack-upgrade/migrations/v1.38.1.0.sh +0 -104
- package/.agent/skills/gstack-upgrade/migrations/v1.40.0.0.sh +0 -144
- package/.agent/skills/gstack-upgrade/migrations/v1.58.0.0.sh +0 -65
- package/.agent/skills/gstack-upgrade/migrations/v1.65.0.0.sh +0 -152
- package/.agent/skills/gstack-upgrade/migrations/v1.67.0.0.sh +0 -73
- package/.agent/skills/gstack-upgrade/migrations/v1.78.0.0.sh +0 -30
- package/.agent/skills/gstack-upgrade/migrations/v1.86.0.0.sh +0 -12
- package/.agent/skills/gstack-upgrade/migrations/v1.91.19.0.sh +0 -53
- package/.agent/skills/gstack-upgrade/migrations/v1.91.65.0.sh +0 -31
- package/.agent/skills/html-renderer/SKILL.md +0 -336
- package/.agent/skills/html-renderer/references/animation-patterns.md +0 -609
- package/.agent/skills/html-renderer/references/character-animation.md +0 -388
- package/.agent/skills/html-renderer/references/collage-broll.md +0 -41
- package/.agent/skills/html-renderer/references/design.md +0 -217
- package/.agent/skills/html-renderer/references/directing.md +0 -165
- package/.agent/skills/html-renderer/references/earshot-audio.md +0 -201
- package/.agent/skills/html-renderer/references/engine-api.md +0 -480
- package/.agent/skills/html-renderer/references/film-styles.md +0 -95
- package/.agent/skills/html-renderer/references/hyperframes-motion.md +0 -208
- package/.agent/skills/html-renderer/references/motion-craft.md +0 -92
- package/.agent/skills/html-renderer/references/onetake-camera.md +0 -59
- package/.agent/skills/html-renderer/references/procedural-sfx.md +0 -147
- package/.agent/skills/html-renderer/references/remotion-patterns.md +0 -159
- package/.agent/skills/html-renderer/references/render-contract.md +0 -66
- package/.agent/skills/motion-styles/LIBRARY.md +0 -896
- package/.agent/skills/motion-styles/README.md +0 -29
- package/.agent/skills/motion-styles/SKILL.md +0 -70
- package/.agent/skills/motion-styles/catalog.json +0 -2253
- package/.agent/skills/motion-styles/prompts/bio-glow.md +0 -149
- package/.agent/skills/motion-styles/prompts/blueprint.md +0 -145
- package/.agent/skills/motion-styles/prompts/breaking-desk.md +0 -147
- package/.agent/skills/motion-styles/prompts/callout-kit.md +0 -147
- package/.agent/skills/motion-styles/prompts/chalk-talk.md +0 -153
- package/.agent/skills/motion-styles/prompts/chat-story.md +0 -143
- package/.agent/skills/motion-styles/prompts/clay-diorama.md +0 -149
- package/.agent/skills/motion-styles/prompts/clean-tech.md +0 -147
- package/.agent/skills/motion-styles/prompts/comic-panels.md +0 -145
- package/.agent/skills/motion-styles/prompts/cosmic-scale.md +0 -147
- package/.agent/skills/motion-styles/prompts/creator-motion.md +0 -149
- package/.agent/skills/motion-styles/prompts/data-city.md +0 -149
- package/.agent/skills/motion-styles/prompts/data-story.md +0 -147
- package/.agent/skills/motion-styles/prompts/deep-layers.md +0 -147
- package/.agent/skills/motion-styles/prompts/doc-cinema.md +0 -145
- package/.agent/skills/motion-styles/prompts/evidence-board.md +0 -145
- package/.agent/skills/motion-styles/prompts/exploded-view.md +0 -149
- package/.agent/skills/motion-styles/prompts/glass-icons.md +0 -147
- package/.agent/skills/motion-styles/prompts/glitch-pack.md +0 -145
- package/.agent/skills/motion-styles/prompts/globe-arcs.md +0 -147
- package/.agent/skills/motion-styles/prompts/headline-backdrop.md +0 -145
- package/.agent/skills/motion-styles/prompts/iso-cutaway.md +0 -147
- package/.agent/skills/motion-styles/prompts/isotype.md +0 -145
- package/.agent/skills/motion-styles/prompts/karaoke-subs.md +0 -145
- package/.agent/skills/motion-styles/prompts/low-poly-atlas.md +0 -149
- package/.agent/skills/motion-styles/prompts/lower-thirds.md +0 -147
- package/.agent/skills/motion-styles/prompts/marker-board.md +0 -145
- package/.agent/skills/motion-styles/prompts/market-tape.md +0 -145
- package/.agent/skills/motion-styles/prompts/math-canvas.md +0 -145
- package/.agent/skills/motion-styles/prompts/myth-or-fact.md +0 -145
- package/.agent/skills/motion-styles/prompts/neon-line.md +0 -145
- package/.agent/skills/motion-styles/prompts/paper-craft.md +0 -145
- package/.agent/skills/motion-styles/prompts/parchment-map.md +0 -147
- package/.agent/skills/motion-styles/prompts/physics-toybox.md +0 -147
- package/.agent/skills/motion-styles/prompts/pixel-quest.md +0 -145
- package/.agent/skills/motion-styles/prompts/podcast-clip.md +0 -147
- package/.agent/skills/motion-styles/prompts/pop-info.md +0 -145
- package/.agent/skills/motion-styles/prompts/product-turntable.md +0 -149
- package/.agent/skills/motion-styles/prompts/punch-captions.md +0 -145
- package/.agent/skills/motion-styles/prompts/quiz-countdown.md +0 -145
- package/.agent/skills/motion-styles/prompts/quote-cinema.md +0 -145
- package/.agent/skills/motion-styles/prompts/recipe-steps.md +0 -145
- package/.agent/skills/motion-styles/prompts/rubber-hose-reel.md +0 -145
- package/.agent/skills/motion-styles/prompts/shadow-puppets.md +0 -145
- package/.agent/skills/motion-styles/prompts/size-lineup.md +0 -149
- package/.agent/skills/motion-styles/prompts/software-walkthrough.md +0 -147
- package/.agent/skills/motion-styles/prompts/stack-split.md +0 -145
- package/.agent/skills/motion-styles/prompts/sticker-pop.md +0 -145
- package/.agent/skills/motion-styles/prompts/storybook.md +0 -145
- package/.agent/skills/motion-styles/prompts/swiss-grid.md +0 -145
- package/.agent/skills/motion-styles/prompts/synth-grid.md +0 -147
- package/.agent/skills/motion-styles/prompts/tactics-board.md +0 -145
- package/.agent/skills/motion-styles/prompts/terminal.md +0 -145
- package/.agent/skills/motion-styles/prompts/tier-list.md +0 -143
- package/.agent/skills/motion-styles/prompts/timeline-rail.md +0 -145
- package/.agent/skills/motion-styles/prompts/top-5-countdown.md +0 -145
- package/.agent/skills/motion-styles/prompts/versus-split.md +0 -145
- package/.agent/skills/motion-styles/prompts/vhs-camcorder.md +0 -145
- package/.agent/skills/motion-styles/prompts/visual-novel.md +0 -145
- package/.agent/skills/motion-styles/prompts/warm-canvas.md +0 -145
- package/.agent/skills/motion-styles/prompts/woodcut-tale.md +0 -145
- package/.agent/skills/motion-styles/prompts/zine-collage.md +0 -145
- package/.agent/skills/motion-styles/sources/bio-glow.html +0 -639
- package/.agent/skills/motion-styles/sources/blueprint.html +0 -617
- package/.agent/skills/motion-styles/sources/breaking-desk.html +0 -585
- package/.agent/skills/motion-styles/sources/callout-kit.html +0 -586
- package/.agent/skills/motion-styles/sources/chalk-talk.html +0 -597
- package/.agent/skills/motion-styles/sources/chat-story.html +0 -597
- package/.agent/skills/motion-styles/sources/clay-diorama.html +0 -681
- package/.agent/skills/motion-styles/sources/clean-tech.html +0 -554
- package/.agent/skills/motion-styles/sources/comic-panels.html +0 -608
- package/.agent/skills/motion-styles/sources/cosmic-scale.html +0 -628
- package/.agent/skills/motion-styles/sources/creator-motion.html +0 -805
- package/.agent/skills/motion-styles/sources/data-city.html +0 -613
- package/.agent/skills/motion-styles/sources/data-story.html +0 -539
- package/.agent/skills/motion-styles/sources/deep-layers.html +0 -624
- package/.agent/skills/motion-styles/sources/doc-cinema.html +0 -590
- package/.agent/skills/motion-styles/sources/evidence-board.html +0 -610
- package/.agent/skills/motion-styles/sources/exploded-view.html +0 -618
- package/.agent/skills/motion-styles/sources/glass-icons.html +0 -612
- package/.agent/skills/motion-styles/sources/glitch-pack.html +0 -583
- package/.agent/skills/motion-styles/sources/globe-arcs.html +0 -621
- package/.agent/skills/motion-styles/sources/headline-backdrop.html +0 -584
- package/.agent/skills/motion-styles/sources/iso-cutaway.html +0 -621
- package/.agent/skills/motion-styles/sources/isotype.html +0 -588
- package/.agent/skills/motion-styles/sources/karaoke-subs.html +0 -577
- package/.agent/skills/motion-styles/sources/low-poly-atlas.html +0 -621
- package/.agent/skills/motion-styles/sources/lower-thirds.html +0 -582
- package/.agent/skills/motion-styles/sources/marker-board.html +0 -598
- package/.agent/skills/motion-styles/sources/market-tape.html +0 -598
- package/.agent/skills/motion-styles/sources/math-canvas.html +0 -592
- package/.agent/skills/motion-styles/sources/myth-or-fact.html +0 -587
- package/.agent/skills/motion-styles/sources/neon-line.html +0 -597
- package/.agent/skills/motion-styles/sources/paper-craft.html +0 -612
- package/.agent/skills/motion-styles/sources/parchment-map.html +0 -624
- package/.agent/skills/motion-styles/sources/physics-toybox.html +0 -617
- package/.agent/skills/motion-styles/sources/pixel-quest.html +0 -609
- package/.agent/skills/motion-styles/sources/podcast-clip.html +0 -585
- package/.agent/skills/motion-styles/sources/pop-info.html +0 -677
- package/.agent/skills/motion-styles/sources/product-turntable.html +0 -611
- package/.agent/skills/motion-styles/sources/punch-captions.html +0 -583
- package/.agent/skills/motion-styles/sources/quiz-countdown.html +0 -593
- package/.agent/skills/motion-styles/sources/quote-cinema.html +0 -581
- package/.agent/skills/motion-styles/sources/recipe-steps.html +0 -599
- package/.agent/skills/motion-styles/sources/rubber-hose-reel.html +0 -627
- package/.agent/skills/motion-styles/sources/shadow-puppets.html +0 -609
- package/.agent/skills/motion-styles/sources/size-lineup.html +0 -620
- package/.agent/skills/motion-styles/sources/software-walkthrough.html +0 -651
- package/.agent/skills/motion-styles/sources/stack-split.html +0 -588
- package/.agent/skills/motion-styles/sources/sticker-pop.html +0 -587
- package/.agent/skills/motion-styles/sources/storybook.html +0 -600
- package/.agent/skills/motion-styles/sources/swiss-grid.html +0 -590
- package/.agent/skills/motion-styles/sources/synth-grid.html +0 -614
- package/.agent/skills/motion-styles/sources/tactics-board.html +0 -608
- package/.agent/skills/motion-styles/sources/terminal.html +0 -593
- package/.agent/skills/motion-styles/sources/tier-list.html +0 -600
- package/.agent/skills/motion-styles/sources/timeline-rail.html +0 -598
- package/.agent/skills/motion-styles/sources/top-5-countdown.html +0 -589
- package/.agent/skills/motion-styles/sources/versus-split.html +0 -592
- package/.agent/skills/motion-styles/sources/vhs-camcorder.html +0 -587
- package/.agent/skills/motion-styles/sources/visual-novel.html +0 -623
- package/.agent/skills/motion-styles/sources/warm-canvas.html +0 -742
- package/.agent/skills/motion-styles/sources/woodcut-tale.html +0 -598
- package/.agent/skills/motion-styles/sources/zine-collage.html +0 -594
- package/.agent/skills/reverse-engineer-anything/SKILL.md +0 -356
- package/.agent/skills/reverse-engineer-anything/references/android-applications.md +0 -36
- package/.agent/skills/reverse-engineer-anything/references/connection-and-recovery.md +0 -127
- package/.agent/skills/reverse-engineer-anything/references/evidence-workflows.md +0 -71
- package/.agent/skills/reverse-engineer-anything/references/javascript-applications.md +0 -218
- package/.agent/skills/reverse-engineer-anything/references/native-and-artifacts.md +0 -297
- package/.agent/skills/reverse-engineer-anything/references/runtime-observation.md +0 -78
- package/.agent/skills/video-pipeline/SKILL.md +0 -384
- package/.agent/skills/video-pipeline/assets/project/ANIMATION_GUIDE.md +0 -258
- package/.agent/skills/video-pipeline/assets/project/STORYBOARD.md +0 -77
- package/.agent/skills/video-pipeline/assets/project/index.html +0 -80
- package/.agent/skills/video-pipeline/assets/project/pipeline/briefings/diagram-author.md +0 -43
- package/.agent/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +0 -75
- package/.agent/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +0 -63
- package/.agent/skills/video-pipeline/assets/project/reel/engine.css +0 -140
- package/.agent/skills/video-pipeline/assets/project/reel/engine.js +0 -2143
- package/.agent/skills/video-pipeline/assets/project/reel/virtual-clock.js +0 -168
- package/.agent/skills/video-pipeline/references/mcp-tools.md +0 -441
- package/.agent/skills/video-pipeline/references/motion-craft.md +0 -91
- package/.agent/skills/video-pipeline/references/providers.md +0 -174
- package/.agent/skills/video-pipeline/references/render-contract.md +0 -65
|
@@ -1,1695 +0,0 @@
|
|
|
1
|
-
import { afterAll, describe, expect, spyOn, test } from 'bun:test';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { appendFileSync, chmodSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { pathToFileURL } from 'node:url';
|
|
8
|
-
import { Database } from 'bun:sqlite';
|
|
9
|
-
import {
|
|
10
|
-
allowedFixturePage, assertDiaSocketPath, assertOwnedDiaProfile, browserCleanupError, browserGroupFacts, browserOperationTimedOut, browserPreflightError, browserRootFacts,
|
|
11
|
-
browserStartupCategory, browserStartupFacts, browserStderrFacts, browserStderrReasons, captureUserKeychains, classifyNativeWaitSample, createBrowserStderrCapture, createOwnedDiaProfile, DIA_DOWNLOAD, fixtureKeychainRestoreCommands,
|
|
12
|
-
hasSandboxDisablingArgument, inspectMachOArchitectures, joinOwnedBrowserClose, macosCompatibility, nativeDiaLaunchOptions, observeBrowserLaunches,
|
|
13
|
-
observePendingBrowserLaunch, sampleOwnedDiaWait, stopOwnedBrowserGroup,
|
|
14
|
-
observeDiaKeychainEnvironments, observeFixtureKeychain, parseDefaultKeychain, parseKeychainPaths, playwrightModuleLoadFacts, prepareKeychainHome,
|
|
15
|
-
qualifyDia, readFreshAccountConfiguration, removeOwnedDiaProfile, validateQualificationHost, writePrivateReceipt,
|
|
16
|
-
} from '../../.github/scripts/qualify-dia-macos';
|
|
17
|
-
import { ARCHIVE_CHECK, FRESH_WORK_PREFIX, PRIVATE_RECEIPT_READ, classifyParentDomain, classifyUserDomain, freshLaunchDefinition, freshQualificationPassed,
|
|
18
|
-
inspectParentDomain, inspectUidProcesses, inspectUserDomain,
|
|
19
|
-
ownedUserDomainTarget, ownsFreshAccount, ownsLaunchService, parseDirectoryIds, parseDirectoryRecord, passiveUserDomainState,
|
|
20
|
-
runFreshAccountQualification, uidProcessFacts } from '../../.github/scripts/run-dia-native-qualification';
|
|
21
|
-
|
|
22
|
-
const require = createRequire(import.meta.url);
|
|
23
|
-
const root = mkdtempSync(path.join(tmpdir(), 'dia-qualification-test-'));
|
|
24
|
-
const script = path.resolve(import.meta.dir, '../../.github/scripts/qualify-dia-macos.ts');
|
|
25
|
-
const nativeEnvironment = {
|
|
26
|
-
CI: 'true', GITHUB_ACTIONS: 'true', RUNNER_ENVIRONMENT: 'github-hosted', RUNNER_OS: 'macOS', RUNNER_ARCH: 'ARM64',
|
|
27
|
-
GSTACK_DIA_NATIVE_QUALIFY: '1', RUNNER_TEMP: root, GITHUB_RUN_ID: 'fixture-run', GITHUB_RUN_ATTEMPT: '1',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
afterAll(() => rmSync(root, { recursive: true, force: true }));
|
|
31
|
-
|
|
32
|
-
describe('Dia macOS CI qualification safety', () => {
|
|
33
|
-
test('admission accepts only the explicitly enabled disposable ARM64 Mac combination', () => {
|
|
34
|
-
expect(() => validateQualificationHost(nativeEnvironment, 'darwin', 'arm64')).not.toThrow();
|
|
35
|
-
for (const [platform, architecture] of [['linux', 'arm64'], ['win32', 'arm64'], ['darwin', 'x64']]) {
|
|
36
|
-
expect(() => validateQualificationHost(nativeEnvironment, platform as any, architecture as any)).toThrow('disposable_arm64_macos_ci_required');
|
|
37
|
-
}
|
|
38
|
-
expect(readdirSync(root)).toEqual([]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
for (const name of Object.keys(nativeEnvironment)) {
|
|
42
|
-
test(`requires admission field ${name} before any native work`, () => {
|
|
43
|
-
const env: NodeJS.ProcessEnv = { ...nativeEnvironment };
|
|
44
|
-
delete env[name];
|
|
45
|
-
expect(() => validateQualificationHost(env, 'darwin', 'arm64')).toThrow('disposable_arm64_macos_ci_required');
|
|
46
|
-
expect(readdirSync(root)).toEqual([]);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
test('rejects a self-hosted machine even when the OS and opt-in match', () => {
|
|
51
|
-
expect(() => validateQualificationHost({ ...nativeEnvironment, RUNNER_ENVIRONMENT: 'self-hosted' }, 'darwin', 'arm64'))
|
|
52
|
-
.toThrow('disposable_arm64_macos_ci_required');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('the actual launcher refuses an unapproved environment without staging files', () => {
|
|
56
|
-
const nativeTemp = mkdtempSync(path.join(root, 'runner-temp-'));
|
|
57
|
-
const result = spawnSync(process.execPath, ['--no-env-file', '--no-install', '--no-macros', `--config=${process.platform === 'win32' ? 'NUL' : '/dev/null'}`, script], {
|
|
58
|
-
cwd: root, env: { PATH: path.dirname(process.execPath), HOME: root, RUNNER_TEMP: nativeTemp,
|
|
59
|
-
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}) },
|
|
60
|
-
encoding: 'utf8', timeout: 10_000,
|
|
61
|
-
});
|
|
62
|
-
expect(result.status).toBe(2);
|
|
63
|
-
expect(result.stderr).toBe('');
|
|
64
|
-
expect(JSON.parse(result.stdout)).toEqual({ status: 'incomplete', reason: 'qualification_preflight_failed', counts: { pass: 0, fail: 0, skip: 0 } });
|
|
65
|
-
expect(readdirSync(nativeTemp)).toEqual([]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('a fresh fixture-HOME child uses unmodified production Dia profile and domain discovery', () => {
|
|
69
|
-
const home = realpathSync(mkdtempSync(path.join(root, 'fixture-home-')));
|
|
70
|
-
const ownership = createOwnedDiaProfile(home);
|
|
71
|
-
const profile = path.join(ownership.profile, 'Default');
|
|
72
|
-
mkdirSync(profile, { recursive: true });
|
|
73
|
-
const database = new Database(path.join(profile, 'Cookies'));
|
|
74
|
-
database.run('CREATE TABLE cookies (host_key TEXT, has_expires INTEGER, expires_utc INTEGER)');
|
|
75
|
-
database.query('INSERT INTO cookies VALUES (?, ?, ?)').run('.fixture.test', 0, 0);
|
|
76
|
-
database.close();
|
|
77
|
-
const production = pathToFileURL(path.resolve(import.meta.dir, '../src/cookie-import-browser.ts')).href;
|
|
78
|
-
const result = spawnSync(process.execPath, ['--no-env-file', '--no-install', '--no-macros', `--config=${process.platform === 'win32' ? 'NUL' : '/dev/null'}`, '-e', `
|
|
79
|
-
import { homedir } from 'node:os';
|
|
80
|
-
const { listProfiles, listDomains } = await import(${JSON.stringify(production)});
|
|
81
|
-
console.log(JSON.stringify({ homeAtStartup: homedir() === process.env.HOME,
|
|
82
|
-
profiles: listProfiles('Dia').map(profile => profile.name), domains: listDomains('Dia', 'Default').domains }));
|
|
83
|
-
`], {
|
|
84
|
-
cwd: root, env: { PATH: path.dirname(process.execPath), HOME: home, USERPROFILE: home,
|
|
85
|
-
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}) },
|
|
86
|
-
encoding: 'utf8', timeout: 10_000,
|
|
87
|
-
});
|
|
88
|
-
expect(result.status).toBe(0);
|
|
89
|
-
expect(result.stderr).toBe('');
|
|
90
|
-
expect(JSON.parse(result.stdout)).toEqual({ homeAtStartup: true, profiles: ['Default'], domains: [{ domain: '.fixture.test', count: 1 }] });
|
|
91
|
-
expect(() => assertOwnedDiaProfile(ownership)).not.toThrow();
|
|
92
|
-
removeOwnedDiaProfile(ownership, true);
|
|
93
|
-
expect(existsSync(home)).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
for (const precreate of [false, true]) {
|
|
97
|
-
test(`late-installed real Playwright loads only with its dependency directory present at startup (${precreate})`, () => {
|
|
98
|
-
const fixture = realpathSync(mkdtempSync(path.join(root, 'late-playwright-')));
|
|
99
|
-
const home = path.join(fixture, 'home');
|
|
100
|
-
const scripts = path.join(fixture, '.github/scripts');
|
|
101
|
-
const modules = path.join(fixture, 'node_modules');
|
|
102
|
-
mkdirSync(home);
|
|
103
|
-
mkdirSync(scripts, { recursive: true });
|
|
104
|
-
if (precreate) mkdirSync(modules, { mode: 0o700 });
|
|
105
|
-
writeFileSync(path.join(fixture, 'package.json'), JSON.stringify({ type: 'module', dependencies: { playwright: '1.62.1' } }));
|
|
106
|
-
const sourceModules = path.resolve(import.meta.dir, '../../node_modules');
|
|
107
|
-
const worker = path.join(scripts, 'worker.ts');
|
|
108
|
-
writeFileSync(worker, `
|
|
109
|
-
import { cpSync, mkdirSync } from 'node:fs';
|
|
110
|
-
import { createRequire } from 'node:module';
|
|
111
|
-
const require = createRequire(import.meta.url);
|
|
112
|
-
mkdirSync(${JSON.stringify(modules)}, { recursive: true });
|
|
113
|
-
for (const name of ['playwright', 'playwright-core']) cpSync(${JSON.stringify(sourceModules)} + '/' + name, ${JSON.stringify(modules)} + '/' + name, { recursive: true });
|
|
114
|
-
const version = require(${JSON.stringify(path.join(modules, 'playwright/package.json'))}).version;
|
|
115
|
-
try {
|
|
116
|
-
const loaded = await import('playwright');
|
|
117
|
-
console.log(JSON.stringify({ version, loaded: !!loaded.chromium }));
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.log(JSON.stringify({ version, type: error.name, code: error.code }));
|
|
120
|
-
process.exitCode = 2;
|
|
121
|
-
}
|
|
122
|
-
`);
|
|
123
|
-
const result = spawnSync(process.execPath, ['--no-env-file', '--no-install', '--no-macros',
|
|
124
|
-
`--config=${process.platform === 'win32' ? 'NUL' : '/dev/null'}`, worker], {
|
|
125
|
-
cwd: fixture, env: { HOME: home, PATH: path.dirname(process.execPath) }, encoding: 'utf8', timeout: 30_000,
|
|
126
|
-
});
|
|
127
|
-
expect(result.stderr).toBe('');
|
|
128
|
-
expect(result.status).toBe(precreate ? 0 : 2);
|
|
129
|
-
expect(JSON.parse(result.stdout)).toEqual(precreate ? { version: '1.62.1', loaded: true }
|
|
130
|
-
: { version: '1.62.1', type: 'ResolveMessage', code: 'ERR_MODULE_NOT_FOUND' });
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
test('the download is the published HTTPS Dia release endpoint', () => {
|
|
135
|
-
expect(DIA_DOWNLOAD).toBe('https://releases.diabrowser.com/release/Dia-latest.dmg');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe('bounded Mach-O architecture inspection', () => {
|
|
139
|
-
const thin = (cpu = 0x0100000c, subtype = 0, little = true, wide = true) => {
|
|
140
|
-
const buffer = Buffer.alloc((wide ? 32 : 28) + 8);
|
|
141
|
-
const word = (value: number, at: number) => little ? buffer.writeUInt32LE(value, at) : buffer.writeUInt32BE(value, at);
|
|
142
|
-
word(wide ? 0xfeedfacf : 0xfeedface, 0);
|
|
143
|
-
word(cpu, 4);
|
|
144
|
-
word(subtype, 8);
|
|
145
|
-
word(2, 12);
|
|
146
|
-
word(1, 16);
|
|
147
|
-
word(8, 20);
|
|
148
|
-
return buffer;
|
|
149
|
-
};
|
|
150
|
-
const fat = (little = false, wide = false) => {
|
|
151
|
-
const buffer = Buffer.alloc(512);
|
|
152
|
-
const word = (value: number, at: number) => little ? buffer.writeUInt32LE(value, at) : buffer.writeUInt32BE(value, at);
|
|
153
|
-
const wideWord = (value: bigint, at: number) => little ? buffer.writeBigUInt64LE(value, at) : buffer.writeBigUInt64BE(value, at);
|
|
154
|
-
word(wide ? 0xcafebabf : 0xcafebabe, 0);
|
|
155
|
-
word(2, 4);
|
|
156
|
-
for (const [index, cpu, subtype] of [[0, 0x01000007, 3], [1, 0x0100000c, 0]]) {
|
|
157
|
-
const at = 8 + index * (wide ? 32 : 20);
|
|
158
|
-
const image = thin(cpu, subtype);
|
|
159
|
-
word(cpu, at);
|
|
160
|
-
word(subtype, at + 4);
|
|
161
|
-
if (wide) { wideWord(BigInt(128 * (index + 1)), at + 8); wideWord(BigInt(image.length), at + 16); }
|
|
162
|
-
else { word(128 * (index + 1), at + 8); word(image.length, at + 12); }
|
|
163
|
-
word(7, at + (wide ? 24 : 16));
|
|
164
|
-
image.copy(buffer, 128 * (index + 1));
|
|
165
|
-
}
|
|
166
|
-
return buffer;
|
|
167
|
-
};
|
|
168
|
-
const store = (buffer: Buffer) => {
|
|
169
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'macho-')));
|
|
170
|
-
const file = path.join(directory, 'executable');
|
|
171
|
-
writeFileSync(file, buffer, { mode: 0o600 });
|
|
172
|
-
return file;
|
|
173
|
-
};
|
|
174
|
-
for (const little of [true, false]) {
|
|
175
|
-
test(`thin headers honor byte order (${little}) and distinguish arm64 subtypes`, () => {
|
|
176
|
-
expect(inspectMachOArchitectures(store(thin(0x0100000c, 0, little))).architectures).toEqual(['arm64']);
|
|
177
|
-
expect(inspectMachOArchitectures(store(thin(0x0100000c, 0x80000002, little))).architectures).toEqual(['arm64e']);
|
|
178
|
-
expect(inspectMachOArchitectures(store(thin(0x01000007, 3, little))).architectures).toEqual(['x86_64']);
|
|
179
|
-
expect(inspectMachOArchitectures(store(thin(7, 3, little, false))).architectures).toEqual(['i386']);
|
|
180
|
-
});
|
|
181
|
-
for (const wide of [true, false]) {
|
|
182
|
-
test(`fat headers verify their actual slices (${little}, ${wide})`, () => {
|
|
183
|
-
const result = inspectMachOArchitectures(store(fat(little, wide)));
|
|
184
|
-
expect(result).toMatchObject({ method: 'bounded_macho_headers', format: wide ? 'fat64' : 'fat32', slices: 2,
|
|
185
|
-
architectures: ['x86_64', 'arm64'] });
|
|
186
|
-
expect(result.bytesRead).toBeLessThanOrEqual(4096);
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
for (const invalid of ['magic', 'truncated', 'empty_fat', 'excessive_fat', 'truncated_table', 'table_overlap', 'slice_overlap',
|
|
192
|
-
'slice_out_of_bounds', 'slice_too_short', 'misaligned', 'alignment_overflow', 'cpu_mismatch', 'subtype_mismatch', 'nested_fat',
|
|
193
|
-
'duplicate_architecture', 'wide_offset_overflow', 'reserved', 'header_width', 'file_type', 'load_commands', 'unknown_cpu']) {
|
|
194
|
-
test(`architecture inspection rejects ${invalid} rather than guessing arm64`, () => {
|
|
195
|
-
let image = fat();
|
|
196
|
-
if (invalid === 'magic') image = Buffer.from('not a macho executable');
|
|
197
|
-
if (invalid === 'truncated') image = thin().subarray(0, 20);
|
|
198
|
-
if (invalid === 'empty_fat') image.writeUInt32BE(0, 4);
|
|
199
|
-
if (invalid === 'excessive_fat') image.writeUInt32BE(33, 4);
|
|
200
|
-
if (invalid === 'truncated_table') image = image.subarray(0, 24);
|
|
201
|
-
if (invalid === 'table_overlap') image.writeUInt32BE(0, 16);
|
|
202
|
-
if (invalid === 'slice_overlap') image.writeUInt32BE(128, 36);
|
|
203
|
-
if (invalid === 'slice_out_of_bounds') image.writeUInt32BE(1024, 36);
|
|
204
|
-
if (invalid === 'slice_too_short') image.writeUInt32BE(28, 40);
|
|
205
|
-
if (invalid === 'misaligned') image.writeUInt32BE(257, 36);
|
|
206
|
-
if (invalid === 'alignment_overflow') image.writeUInt32BE(0xffffffff, 44);
|
|
207
|
-
if (invalid === 'cpu_mismatch') image.writeUInt32LE(0x01000007, 260);
|
|
208
|
-
if (invalid === 'subtype_mismatch') image.writeUInt32LE(2, 264);
|
|
209
|
-
if (invalid === 'nested_fat') image.writeUInt32BE(0xcafebabe, 256);
|
|
210
|
-
if (invalid === 'duplicate_architecture') { image.writeUInt32BE(0x01000007, 28); image.writeUInt32BE(3, 32); thin(0x01000007, 3).copy(image, 256); }
|
|
211
|
-
if (invalid === 'wide_offset_overflow') { image = fat(false, true); image.writeBigUInt64BE(1n << 60n, 16); }
|
|
212
|
-
if (invalid === 'reserved') { image = fat(false, true); image.writeUInt32BE(1, 36); }
|
|
213
|
-
if (invalid === 'header_width') image = thin(0x0100000c, 0, true, false);
|
|
214
|
-
if (invalid === 'file_type') { image = thin(); image.writeUInt32LE(6, 12); }
|
|
215
|
-
if (invalid === 'load_commands') { image = thin(); image.writeUInt32LE(1000, 20); }
|
|
216
|
-
if (invalid === 'unknown_cpu') image = thin(0x010000ff, 0);
|
|
217
|
-
expect(() => inspectMachOArchitectures(store(image))).toThrow();
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
test('architecture inspection rejects links, non-files, and expired budgets', () => {
|
|
222
|
-
const file = store(thin());
|
|
223
|
-
symlinkSync(file, file + '.link');
|
|
224
|
-
expect(() => inspectMachOArchitectures(file + '.link')).toThrow('unsafe_macho_file');
|
|
225
|
-
expect(() => inspectMachOArchitectures(path.dirname(file))).toThrow('unsafe_macho_file');
|
|
226
|
-
for (const budget of [0, NaN, Infinity]) expect(() => inspectMachOArchitectures(file, budget)).toThrow('macho_read_budget_exhausted');
|
|
227
|
-
expect(qualifyDia.toString()).not.toContain('/usr/bin/lipo');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
for (const change of ['append', 'replace']) {
|
|
231
|
-
test(`architecture inspection detects a file that changes by ${change}`, () => {
|
|
232
|
-
const image = thin();
|
|
233
|
-
const file = store(image);
|
|
234
|
-
let ticks = 0;
|
|
235
|
-
const clock = spyOn(performance, 'now').mockImplementation(() => {
|
|
236
|
-
if (++ticks === 3) {
|
|
237
|
-
if (change === 'append') appendFileSync(file, Buffer.from([0]));
|
|
238
|
-
else { renameSync(file, file + '.previous'); writeFileSync(file, image); }
|
|
239
|
-
}
|
|
240
|
-
return 100;
|
|
241
|
-
});
|
|
242
|
-
try { expect(() => inspectMachOArchitectures(file)).toThrow('macho_changed_during_inspection'); }
|
|
243
|
-
finally { clock.mockRestore(); }
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
test('the exact nested macOS account and qualifier socket layout stays below the unchanged limit', () => {
|
|
248
|
-
const sourceProfile = path.posix.join(FRESH_WORK_PREFIX + 'XXXXXX', 'tmp', 'dia-XXXXXX', 'h', 'Library/Application Support/Dia/User Data');
|
|
249
|
-
expect(Buffer.byteLength(sourceProfile + '/SingletonSocket')).toBe(97);
|
|
250
|
-
expect(() => assertDiaSocketPath(sourceProfile)).not.toThrow();
|
|
251
|
-
const oldProfile = '/private/tmp/dia-native-XXXXXX/tmp/dia-XXXXXX/h/Library/Application Support/Dia/User Data';
|
|
252
|
-
expect(Buffer.byteLength(oldProfile + '/SingletonSocket')).toBe(105);
|
|
253
|
-
expect(() => assertDiaSocketPath(oldProfile)).toThrow('fixture_socket_path_too_long');
|
|
254
|
-
const atLimit = '/' + 'x'.repeat(100 - Buffer.byteLength('//SingletonSocket'));
|
|
255
|
-
expect(Buffer.byteLength(atLimit + '/SingletonSocket')).toBe(100);
|
|
256
|
-
expect(() => assertDiaSocketPath(atLimit)).toThrow('fixture_socket_path_too_long');
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test('the registered-home source profile fits the socket bound without a shadow HOME', () => {
|
|
260
|
-
const profile = path.posix.join(FRESH_WORK_PREFIX + 'XXXXXX', 'home', 'Library/Application Support/Dia/User Data');
|
|
261
|
-
expect(Buffer.byteLength(profile + '/SingletonSocket')).toBe(85);
|
|
262
|
-
expect(() => assertDiaSocketPath(profile)).not.toThrow();
|
|
263
|
-
const implementation = qualifyDia.toString();
|
|
264
|
-
expect(implementation).toContain('readFreshAccountConfiguration');
|
|
265
|
-
expect(implementation).toContain('const home = account.home');
|
|
266
|
-
expect(implementation).not.toMatch(/fixtureEnvironment|systemEnvironment|isolation\.originalHome/);
|
|
267
|
-
expect(implementation).toContain('removeOwnedDiaProfile');
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test('profile ownership is exclusive and cleanup preserves the account home and sibling state', () => {
|
|
271
|
-
const home = realpathSync(mkdtempSync(path.join(root, 'owned-profile-home-')));
|
|
272
|
-
prepareKeychainHome(home);
|
|
273
|
-
const preserved = path.join(home, 'Library/Keychains/fixture-state');
|
|
274
|
-
writeFileSync(preserved, 'preserved fixture state');
|
|
275
|
-
const ownership = createOwnedDiaProfile(home);
|
|
276
|
-
const sibling = path.join(path.dirname(ownership.profile), 'sibling-state');
|
|
277
|
-
writeFileSync(sibling, 'preserved sibling state');
|
|
278
|
-
expect(() => assertOwnedDiaProfile(ownership)).not.toThrow();
|
|
279
|
-
expect(() => createOwnedDiaProfile(home)).toThrow();
|
|
280
|
-
expect(() => removeOwnedDiaProfile(ownership, false)).toThrow('owned_browsers_not_stopped');
|
|
281
|
-
expect(existsSync(ownership.profile)).toBe(true);
|
|
282
|
-
removeOwnedDiaProfile(ownership, true);
|
|
283
|
-
expect(existsSync(ownership.profile)).toBe(false);
|
|
284
|
-
expect(existsSync(home)).toBe(true);
|
|
285
|
-
expect(readFileSync(preserved, 'utf8')).toBe('preserved fixture state');
|
|
286
|
-
expect(readFileSync(sibling, 'utf8')).toBe('preserved sibling state');
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
for (const change of ['nonce', 'inode', 'marker_symlink', 'profile_symlink', 'uid', 'home_escape', 'marker_permissions', 'marker_hardlink']) {
|
|
290
|
-
test(`profile cleanup refuses changed ${change} ownership without deleting the directory`, () => {
|
|
291
|
-
const home = realpathSync(mkdtempSync(path.join(root, 'profile-refusal-')));
|
|
292
|
-
const ownership = createOwnedDiaProfile(home);
|
|
293
|
-
const profile = ownership.profile;
|
|
294
|
-
const marker = path.join(profile, '.gstack-dia-owner');
|
|
295
|
-
if (change === 'nonce') writeFileSync(marker, (ownership.nonce[0] === '0' ? '1' : '0') + ownership.nonce.slice(1));
|
|
296
|
-
if (change === 'inode') {
|
|
297
|
-
renameSync(profile, profile + '.original');
|
|
298
|
-
mkdirSync(profile, { mode: 0o700 });
|
|
299
|
-
writeFileSync(marker, ownership.nonce, { mode: 0o600 });
|
|
300
|
-
}
|
|
301
|
-
if (change === 'marker_symlink') { renameSync(marker, marker + '.original'); symlinkSync(marker + '.original', marker); }
|
|
302
|
-
if (change === 'profile_symlink') { renameSync(profile, profile + '.original'); symlinkSync(profile + '.original', profile, 'dir'); }
|
|
303
|
-
if (change === 'uid') ownership.uid = process.getuid!() + 1;
|
|
304
|
-
if (change === 'home_escape') ownership.profile = home;
|
|
305
|
-
if (change === 'marker_permissions') chmodSync(marker, 0o644);
|
|
306
|
-
if (change === 'marker_hardlink') linkSync(marker, path.join(home, 'marker-link'));
|
|
307
|
-
expect(() => removeOwnedDiaProfile(ownership, true)).toThrow();
|
|
308
|
-
expect(existsSync(profile)).toBe(true);
|
|
309
|
-
expect(existsSync(home)).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
test('source profile creation refuses existing profiles and linked ancestors without writing through them', () => {
|
|
314
|
-
const home = realpathSync(mkdtempSync(path.join(root, 'existing-profile-')));
|
|
315
|
-
const profile = path.join(home, 'Library/Application Support/Dia/User Data');
|
|
316
|
-
mkdirSync(profile, { recursive: true });
|
|
317
|
-
writeFileSync(path.join(profile, 'existing-state'), 'untouched');
|
|
318
|
-
expect(() => createOwnedDiaProfile(home)).toThrow();
|
|
319
|
-
expect(readdirSync(profile)).toEqual(['existing-state']);
|
|
320
|
-
const linkedHome = realpathSync(mkdtempSync(path.join(root, 'linked-profile-home-')));
|
|
321
|
-
const elsewhere = realpathSync(mkdtempSync(path.join(root, 'linked-profile-target-')));
|
|
322
|
-
symlinkSync(elsewhere, path.join(linkedHome, 'Library'), 'dir');
|
|
323
|
-
expect(() => createOwnedDiaProfile(linkedHome)).toThrow('unsafe_profile_ancestor');
|
|
324
|
-
expect(readdirSync(elsewhere)).toEqual([]);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
test('the qualifier rejects an arbitrary caller-supplied home as fresh account authority', () => {
|
|
328
|
-
const fake = path.join(root, 'account.json');
|
|
329
|
-
writeFileSync(fake, JSON.stringify({ home: root, uid: process.getuid?.() }));
|
|
330
|
-
expect(() => readFreshAccountConfiguration(fake)).toThrow('unsafe_fresh_account_configuration');
|
|
331
|
-
expect(() => readFreshAccountConfiguration('account.json')).toThrow('unsafe_fresh_account_configuration');
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test('headless source launch removes mock Keychain and first-run suppression defaults', () => {
|
|
335
|
-
const env = { HOME: '/fixture/home', PATH: '/usr/bin:/bin' };
|
|
336
|
-
const options = nativeDiaLaunchOptions('/fixture/Dia.app/Contents/MacOS/Dia', env);
|
|
337
|
-
expect(options.headless).toBe(true);
|
|
338
|
-
expect(options.chromiumSandbox).toBe(true);
|
|
339
|
-
expect(options.timeout).toBe(30_000);
|
|
340
|
-
expect(options.ignoreDefaultArgs).toEqual(['--use-mock-keychain', '--password-store=basic', '--no-first-run']);
|
|
341
|
-
expect(options.args).toEqual(['--disable-sync', '--no-default-browser-check', '--profile-directory=Default']);
|
|
342
|
-
expect(options.env).toBe(env);
|
|
343
|
-
expect(options.serviceWorkers).toBe('block');
|
|
344
|
-
expect(options.args.some(arg => /onboarding|skip-login|remote-debugging-port/.test(arg))).toBe(false);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test('Mac native spawn policy refuses sandbox-disabling arguments regardless of caller options', () => {
|
|
348
|
-
const descriptor = Object.getOwnPropertyDescriptor(process, 'platform')!;
|
|
349
|
-
const profile = path.join(root, 'mac-sandbox-policy');
|
|
350
|
-
const childProcess = require('node:child_process');
|
|
351
|
-
const observer = observeBrowserLaunches(new Map([[process.execPath, profile]]));
|
|
352
|
-
try {
|
|
353
|
-
Object.defineProperty(process, 'platform', { ...descriptor, value: 'darwin' });
|
|
354
|
-
for (const disabling of [['--no-sandbox'], ['--no-sandbox=false'], ['--disable-sandbox'], ['--disable-gpu-sandbox'],
|
|
355
|
-
['--disable-setuid-sandbox'], ['--disable-seccomp-filter-sandbox'], ['--disable-namespace-sandbox'], ['--no-zygote-sandbox'],
|
|
356
|
-
['--single-process'], ['--in-process-gpu'], ['--disable-features=GpuSandboxV2'], ['--disable-features', 'RendererSandbox']]) {
|
|
357
|
-
expect(hasSandboxDisablingArgument(disabling)).toBe(true);
|
|
358
|
-
expect(() => childProcess.spawn(process.execPath, ['--remote-debugging-pipe', '--user-data-dir=' + profile, ...disabling], {
|
|
359
|
-
detached: true, shell: false, stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'], chromiumSandbox: false,
|
|
360
|
-
allowSandboxDisable: true, env: { DISABLE_SANDBOX: '1' },
|
|
361
|
-
})).toThrow('browser_launch_policy_rejected');
|
|
362
|
-
expect(observer.attempts.at(-1)).toMatchObject({ sandboxRequired: true, sandboxDisablingFlag: true });
|
|
363
|
-
}
|
|
364
|
-
expect(observer.children).toHaveLength(0);
|
|
365
|
-
} finally { Object.defineProperty(process, 'platform', descriptor); observer.restore(); }
|
|
366
|
-
expect(hasSandboxDisablingArgument(['--headless', '--disable-sync', '--disable-features=MediaRouter,Translate'])).toBe(false);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
test('startup admits only blank pages or the exact synthetic loopback origin', () => {
|
|
370
|
-
const origin = 'http://127.0.0.1:8123';
|
|
371
|
-
expect(allowedFixturePage('about:blank', origin)).toBe(true);
|
|
372
|
-
expect(allowedFixturePage(origin + '/seed', origin)).toBe(true);
|
|
373
|
-
for (const url of ['https://www.diabrowser.com/login', 'dia://onboarding', 'chrome://welcome', 'http://localhost:8123/seed',
|
|
374
|
-
'http://127.0.0.1:8124/seed', 'javascript:alert(1)', 'about:config', 'invalid']) expect(allowedFixturePage(url, origin)).toBe(false);
|
|
375
|
-
const authenticated = new URL(origin);
|
|
376
|
-
authenticated.username = 'fixture-user';
|
|
377
|
-
authenticated.password = 'fixture';
|
|
378
|
-
expect(allowedFixturePage(authenticated.href, origin)).toBe(false);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test('startup diagnostics classify pages without exposing URLs or broadening admission', () => {
|
|
382
|
-
const pages = [
|
|
383
|
-
['about:blank', 'blank'], ['about:blank#synthetic-private-value', 'other_about'],
|
|
384
|
-
['chrome://newtab/?token=synthetic-private-value', 'chromium_new_tab'],
|
|
385
|
-
['chrome://new-tab-page/', 'chromium_new_tab'], ['chrome://intro/', 'chromium_onboarding'],
|
|
386
|
-
['chrome://welcome/', 'chromium_onboarding'], ['chrome://settings/', 'chromium_internal'],
|
|
387
|
-
['dia://onboarding', 'dia_internal'], ['chrome-extension://fixture/path', 'extension'],
|
|
388
|
-
['https://fixture.invalid/login?token=synthetic-private-value', 'external_web'],
|
|
389
|
-
['http://127.0.0.1:8123' + '/fixture', 'loopback_web'], ['file:///synthetic-private-value', 'file'],
|
|
390
|
-
['data:text/html,synthetic-private-value', 'data'], ['invalid synthetic-private-value', 'invalid'],
|
|
391
|
-
];
|
|
392
|
-
const categories = pages.map(([url]) => browserStartupCategory(url));
|
|
393
|
-
expect(categories).toEqual(pages.map(([, category]) => category));
|
|
394
|
-
expect(JSON.stringify(categories)).not.toContain('synthetic-private-value');
|
|
395
|
-
expect(JSON.stringify(categories)).not.toContain('fixture.invalid');
|
|
396
|
-
for (const [url] of pages.slice(1, 10)) expect(allowedFixturePage(url, 'http://127.0.0.1:8123')).toBe(false);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test('browser stderr classifies only known policy, pipe, Keychain, loader and bootstrap diagnostics', () => {
|
|
400
|
-
for (const [line, reason] of [
|
|
401
|
-
['DevTools remote debugging requires a non-default data directory. Specify this using --user-data-dir.', 'default_profile_policy'],
|
|
402
|
-
['Remote debugging pipe file descriptors are not open.', 'debugging_pipe_unavailable'],
|
|
403
|
-
['errSecInteractionNotAllowed: User interaction is not allowed.', 'keychain_interaction_disallowed'],
|
|
404
|
-
['errSecInteractionRequired', 'keychain_interaction_required'],
|
|
405
|
-
['errSecAuthFailed', 'keychain_access_failed'],
|
|
406
|
-
['dyld[123]: Library not loaded:', 'dynamic_library_error'],
|
|
407
|
-
['code signature invalid', 'code_signing_error'],
|
|
408
|
-
['bootstrap_check_in failed', 'graphics_or_bootstrap_error'],
|
|
409
|
-
['ProcessSingleton', 'browser_profile_unavailable'],
|
|
410
|
-
]) {
|
|
411
|
-
const reasons = browserStderrReasons(line + ' synthetic-private-value /private/fixture/profile');
|
|
412
|
-
expect(reasons).toContain(reason);
|
|
413
|
-
expect(JSON.stringify(reasons)).not.toContain('synthetic-private');
|
|
414
|
-
expect(JSON.stringify(reasons)).not.toContain('/private');
|
|
415
|
-
}
|
|
416
|
-
for (const line of ['using a non-default data directory', 'remote-debugging-pipe enabled', 'Waiting for Keychain',
|
|
417
|
-
'WindowServer connection available', 'private unknown diagnostic']) expect(browserStderrReasons(line)).toEqual([]);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
test('stderr capture handles chunk boundaries and final unterminated lines without retaining text', () => {
|
|
421
|
-
const capture = createBrowserStderrCapture();
|
|
422
|
-
const policy = 'DevTools remote debugging requires a non-default data directory.';
|
|
423
|
-
capture.consume(Buffer.from('synthetic-private-value\n' + policy.slice(0, 23)));
|
|
424
|
-
expect(capture.snapshot().reasonCounts.default_profile_policy).toBe(0);
|
|
425
|
-
capture.consume(Buffer.from(policy.slice(23)));
|
|
426
|
-
const before = capture.snapshot();
|
|
427
|
-
expect(before.reasonCounts.default_profile_policy).toBe(1);
|
|
428
|
-
capture.consume(Buffer.from('\n' + policy));
|
|
429
|
-
capture.end();
|
|
430
|
-
expect(capture.snapshot().reasonCounts.default_profile_policy).toBe(2);
|
|
431
|
-
expect(before.reasonCounts.default_profile_policy).toBe(1);
|
|
432
|
-
expect(capture.snapshot().ended).toBe(true);
|
|
433
|
-
expect(JSON.stringify(capture.snapshot())).not.toContain('synthetic-private');
|
|
434
|
-
expect(JSON.stringify(capture.snapshot())).not.toContain('DevTools');
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
test('stderr inspection caps bytes and pending line size while exposing truncation honestly', () => {
|
|
438
|
-
const capture = createBrowserStderrCapture();
|
|
439
|
-
capture.consume(Buffer.alloc(65_536, 120));
|
|
440
|
-
capture.consume(Buffer.from('\nDevTools remote debugging requires a non-default data directory.\n'));
|
|
441
|
-
const facts = capture.snapshot();
|
|
442
|
-
expect(facts.bytesInspected).toBe(65_536);
|
|
443
|
-
expect(facts.bytesSeen).toBeGreaterThan(65_536);
|
|
444
|
-
expect(facts.truncated).toBe(true);
|
|
445
|
-
expect(facts.discardedLongLines).toBe(1);
|
|
446
|
-
expect(facts.reasonCounts.default_profile_policy).toBe(0);
|
|
447
|
-
const lines = createBrowserStderrCapture();
|
|
448
|
-
lines.consume('x'.repeat(5000) + '\nRemote debugging pipe file descriptors are not open.\n');
|
|
449
|
-
expect(lines.snapshot().reasonCounts.debugging_pipe_unavailable).toBe(1);
|
|
450
|
-
expect(lines.snapshot().truncated).toBe(true);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
test('causal stderr reasons outrank a generic timeout without changing its timeout identity', () => {
|
|
454
|
-
const timeout = Object.assign(new Error('synthetic-private-timeout'), { name: 'TimeoutError' });
|
|
455
|
-
expect(browserOperationTimedOut(timeout)).toBe(true);
|
|
456
|
-
expect(browserPreflightError(timeout)).toBe('operation_timeout');
|
|
457
|
-
expect(browserPreflightError(timeout, ['default_profile_policy'])).toBe('default_profile_policy');
|
|
458
|
-
expect(browserPreflightError(timeout, ['debugging_pipe_unavailable'])).toBe('debugging_pipe_unavailable');
|
|
459
|
-
expect(browserPreflightError(timeout, ['keychain_interaction_required'])).toBe('keychain_interaction_required');
|
|
460
|
-
const embedded = Object.assign(new Error('Timed out\nDevTools remote debugging requires a non-default data directory.'), { name: 'TimeoutError' });
|
|
461
|
-
expect(browserPreflightError(embedded)).toBe('default_profile_policy');
|
|
462
|
-
expect(browserOperationTimedOut(embedded)).toBe(true);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
test('stderr snapshots keep source, destination, and cleanup-time observations separate', () => {
|
|
466
|
-
const source = createBrowserStderrCapture();
|
|
467
|
-
const destination = createBrowserStderrCapture();
|
|
468
|
-
source.consume('DevTools remote debugging requires a non-default data directory.\n');
|
|
469
|
-
const beforeCleanup = source.snapshot();
|
|
470
|
-
source.consume('bootstrap_check_in failed\n');
|
|
471
|
-
destination.consume('Remote debugging pipe file descriptors are not open.\n');
|
|
472
|
-
expect(beforeCleanup.reasonCounts.graphics_or_bootstrap_error).toBe(0);
|
|
473
|
-
expect(source.snapshot().reasonCounts.graphics_or_bootstrap_error).toBe(1);
|
|
474
|
-
expect(destination.snapshot().reasonCounts.default_profile_policy).toBe(0);
|
|
475
|
-
expect(destination.snapshot().reasonCounts.debugging_pipe_unavailable).toBe(1);
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test('signed macOS requirements compare numeric versions and preserve unknown metadata', () => {
|
|
479
|
-
expect(macosCompatibility({ LSMinimumSystemVersion: '14.0' }, '15.6.1')).toEqual({ hostVersion: '15.6.1',
|
|
480
|
-
minimumSystemVersion: '14.0', minimumArm64Version: null, compatible: true });
|
|
481
|
-
expect(macosCompatibility({ LSMinimumSystemVersion: '26.0' }, '15.6.1').compatible).toBe(false);
|
|
482
|
-
expect(macosCompatibility({ LSMinimumSystemVersion: '15.9' }, '15.10').compatible).toBe(true);
|
|
483
|
-
expect(macosCompatibility({ LSMinimumSystemVersion: '15.6.1' }, '15.6').compatible).toBe(false);
|
|
484
|
-
expect(macosCompatibility({ LSMinimumSystemVersion: '15.6' }, '15.6.0').compatible).toBe(true);
|
|
485
|
-
expect(macosCompatibility({ LSMinimumSystemVersion: '14.0', LSMinimumSystemVersionByArchitecture: { arm64: '26.0' } }, '15.6.1').compatible).toBe(false);
|
|
486
|
-
expect(macosCompatibility({ LSMinimumSystemVersion: '26.0', LSMinimumSystemVersionByArchitecture: { arm64: '14.0' } }, '15.6.1').compatible).toBe(false);
|
|
487
|
-
expect(macosCompatibility({ LSMinimumSystemVersionByArchitecture: { arm64: '14.0' } }, '15.6.1').compatible).toBe(true);
|
|
488
|
-
for (const plist of [{}, null, { LSMinimumSystemVersion: 'synthetic-private-value' }, { LSMinimumSystemVersion: 14 },
|
|
489
|
-
{ LSMinimumSystemVersion: '14.0', LSMinimumSystemVersionByArchitecture: 'synthetic-private-value' },
|
|
490
|
-
{ LSMinimumSystemVersion: '14.0', LSMinimumSystemVersionByArchitecture: [] },
|
|
491
|
-
{ LSMinimumSystemVersion: '14.0', LSMinimumSystemVersionByArchitecture: { arm64: 'private-version' } }]) {
|
|
492
|
-
const facts = macosCompatibility(plist, '15.6.1');
|
|
493
|
-
expect(facts.compatible).toBeNull();
|
|
494
|
-
expect(JSON.stringify(facts)).not.toContain('private');
|
|
495
|
-
}
|
|
496
|
-
expect(macosCompatibility({ LSMinimumSystemVersion: '14.0' }, 'private-host-version').compatible).toBeNull();
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
test('bounded startup categories do not hide a disallowed page beyond the receipt limit', () => {
|
|
500
|
-
const urls = [...Array(64).fill('about:blank'), 'https://private.invalid/?token=synthetic-private-value'];
|
|
501
|
-
const facts = browserStartupFacts(urls, 'http://127.0.0.1:8123');
|
|
502
|
-
expect(facts).toMatchObject({ count: 65, truncated: true, allowed: false });
|
|
503
|
-
expect(facts.categories).toHaveLength(64);
|
|
504
|
-
expect(JSON.stringify(facts)).not.toContain('private');
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
test('browser root facts retain exit and allowlisted signal evidence without process payloads', () => {
|
|
508
|
-
const facts = browserRootFacts([
|
|
509
|
-
{ pid: 300, process: { exitCode: null, signalCode: null, spawnargs: ['synthetic-private-value'] } },
|
|
510
|
-
{ pid: 301, process: { exitCode: 0, signalCode: null } },
|
|
511
|
-
{ pid: 302, process: { exitCode: null, signalCode: 'SIGABRT' } },
|
|
512
|
-
{ pid: 303, process: { exitCode: null, signalCode: 'synthetic-private-signal' } },
|
|
513
|
-
] as any);
|
|
514
|
-
expect(facts).toEqual([{ pid: 300, exitCode: null, signal: null }, { pid: 301, exitCode: 0, signal: null },
|
|
515
|
-
{ pid: 302, exitCode: null, signal: 'SIGABRT' }, { pid: 303, exitCode: null, signal: 'other' }]);
|
|
516
|
-
expect(JSON.stringify(facts)).not.toContain('private');
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
test('browser cleanup distinguishes live groups from signal errors without retaining raw errors', () => {
|
|
520
|
-
expect(browserCleanupError(new Error('owned_process_group_still_live')).reason).toBe('group_still_live');
|
|
521
|
-
expect(browserCleanupError(new Error('cleanup_budget_exhausted')).reason).toBe('cleanup_deadline');
|
|
522
|
-
expect(browserCleanupError(Object.assign(new Error('synthetic-private-value'), { code: 'EPERM', errno: 1 })))
|
|
523
|
-
.toEqual({ reason: 'signal_or_probe_failed', code: 'EPERM', errno: 1 });
|
|
524
|
-
const unknown = browserCleanupError({ message: 'synthetic-private-value', code: 'private-code', errno: 'private-errno' });
|
|
525
|
-
expect(unknown).toEqual({ reason: 'signal_or_probe_failed', code: 'unclassified', errno: null });
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
test('process-group diagnostics distinguish owned zombies, live members, and unrelated UID counts', () => {
|
|
529
|
-
const facts = browserGroupFacts('20000 300 1 300 Z\n20000 301 1 300 S+\n501 302 1 300 S\n20000 400 1 400 S\n', 20000, 300);
|
|
530
|
-
expect(facts).toEqual({ available: true, count: 2, foreignUidCount: 1, zombies: 1, live: 1, truncated: false,
|
|
531
|
-
processes: [{ pid: 300, ppid: 1, state: 'Z' }, { pid: 301, ppid: 1, state: 'S' }] });
|
|
532
|
-
expect(browserGroupFacts(Array.from({ length: 70 }, (_, index) => `20000 ${index + 300} 1 300 S`).join('\n'), 20000, 300).processes).toHaveLength(64);
|
|
533
|
-
expect(() => browserGroupFacts('private-invalid-row', 20000, 300)).toThrow('invalid_group_snapshot');
|
|
534
|
-
expect(() => browserGroupFacts('', 20000, 0)).toThrow('invalid_group_snapshot_target');
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
test('native samples retain fixed wait families only from call-graph frames', () => {
|
|
538
|
-
const output = `Process: private-process [123]
|
|
539
|
-
Path: /private/sensitive/Security/AppKit/CFNetwork
|
|
540
|
-
Call graph:
|
|
541
|
-
100 Thread_11 DispatchQueue_1: com.apple.main-thread (serial)
|
|
542
|
-
+ 100 NSApplicationMain (in AppKit) private-source-path
|
|
543
|
-
+ 100 SecKeychainFindGenericPassword (in Security) private-item-name
|
|
544
|
-
+ 100 mach_msg_trap (in libsystem_kernel)
|
|
545
|
-
100 Thread_12 private-thread-name
|
|
546
|
-
+ 100 NSURLSessionTask (in CFNetwork) private-request-url
|
|
547
|
-
Total number in stack (recursive counted multiple):
|
|
548
|
-
999 SecItemCopyMatching private-unrelated-summary
|
|
549
|
-
Binary Images:
|
|
550
|
-
Security AppKit CFNetwork /private/image/path
|
|
551
|
-
`;
|
|
552
|
-
const result = classifyNativeWaitSample(output);
|
|
553
|
-
expect(result).toEqual({ available: true, callGraphSeen: true, mainThreadSeen: true, frames: 4,
|
|
554
|
-
frameCounts: { security_keychain: 1, appkit_bootstrap: 1, network: 1, runloop: 1 },
|
|
555
|
-
mainThreadFrameCounts: { security_keychain: 1, appkit_bootstrap: 1, network: 0, runloop: 1 },
|
|
556
|
-
shape: { graphLines: 6, nonemptyGraphLines: 6, threadTokenLines: 2, numericPrefixLines: 2, imageAnnotatedLines: 4,
|
|
557
|
-
nonAsciiGraphLines: 0, unrecognizedGraphLines: 0, graphEnd: 'totals', binaryImagesSeen: true } });
|
|
558
|
-
expect(JSON.stringify(result)).not.toContain('private');
|
|
559
|
-
expect(JSON.stringify(result)).not.toContain('SecKeychain');
|
|
560
|
-
expect(classifyNativeWaitSample('private missing call graph').available).toBe(false);
|
|
561
|
-
expect(classifyNativeWaitSample('x'.repeat(1024 * 1024 + 1))).toEqual({ available: false, reason: 'sample_output_oversized' });
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
test('sample structure distinguishes an empty graph from unrecognized stack rows without exposing text', () => {
|
|
565
|
-
const empty = classifyNativeWaitSample('Call graph:\n\nTotal number in stack:\nBinary Images:\nprivate-image');
|
|
566
|
-
expect(empty).toMatchObject({ available: false, frames: 0, callGraphSeen: true,
|
|
567
|
-
shape: { graphLines: 1, nonemptyGraphLines: 0, unrecognizedGraphLines: 0, graphEnd: 'totals', binaryImagesSeen: true } });
|
|
568
|
-
const unfamiliar = classifyNativeWaitSample('Call graph:\n ◇ private-row (in Security)\nBinary Images:\nprivate-image');
|
|
569
|
-
expect(unfamiliar).toMatchObject({ available: false, frames: 0,
|
|
570
|
-
shape: { nonemptyGraphLines: 1, unrecognizedGraphLines: 1, nonAsciiGraphLines: 1, imageAnnotatedLines: 1, graphEnd: 'binary_images' } });
|
|
571
|
-
expect(JSON.stringify(unfamiliar)).not.toContain('private');
|
|
572
|
-
expect(JSON.stringify(unfamiliar)).not.toContain('Security');
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
test('a pending launch is sampled once with a deadline before the native launch timeout', async () => {
|
|
576
|
-
let resolve!: (value: string) => void;
|
|
577
|
-
const pending = new Promise<string>(done => { resolve = done; });
|
|
578
|
-
const started = performance.now();
|
|
579
|
-
const deadlines: number[] = [];
|
|
580
|
-
const result = await observePendingBrowserLaunch(() => pending, deadline => { deadlines.push(deadline); resolve('ready'); }, started + 60_000, 5);
|
|
581
|
-
expect(result).toBe('ready');
|
|
582
|
-
expect(deadlines).toHaveLength(1);
|
|
583
|
-
expect(deadlines[0]).toBeLessThan(started + 30_000);
|
|
584
|
-
expect(deadlines[0]).toBeGreaterThan(started + 24_000);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
test('settled launches cancel sampling and exhausted sampling windows do not extend the launch', async () => {
|
|
588
|
-
let observations = 0;
|
|
589
|
-
const observe = () => { observations++; };
|
|
590
|
-
expect(await observePendingBrowserLaunch(async () => 'ready', observe, performance.now() + 60_000, 5)).toBe('ready');
|
|
591
|
-
const original = new Error('original_launch_failure');
|
|
592
|
-
await expect(observePendingBrowserLaunch(async () => { throw original; }, observe, performance.now() + 60_000, 5)).rejects.toBe(original);
|
|
593
|
-
expect(await observePendingBrowserLaunch(async () => { await Bun.sleep(10); return 'ready'; }, observe, performance.now() + 100, 0)).toBe('ready');
|
|
594
|
-
await Bun.sleep(10);
|
|
595
|
-
expect(observations).toBe(0);
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
test('native wait sampling uses only the observed live child and never writes a stack artifact', () => {
|
|
599
|
-
const pid = 12345;
|
|
600
|
-
const uid = process.getuid!();
|
|
601
|
-
const child: any = { pid, executable: '/owned/Dia', closeObserved: false, process: { pid, exitCode: null, signalCode: null } };
|
|
602
|
-
const calls: string[][] = [];
|
|
603
|
-
const result = sampleOwnedDiaWait(child, uid, child.executable, performance.now() + 10_000, { HOME: root },
|
|
604
|
-
((command: string, args: string[], options: any) => {
|
|
605
|
-
calls.push([command, ...args]);
|
|
606
|
-
expect(Number.isInteger(options.timeout)).toBe(true);
|
|
607
|
-
expect(options.timeout).toBeGreaterThan(0);
|
|
608
|
-
expect(options.timeout).toBeLessThanOrEqual(5_000);
|
|
609
|
-
expect(options.killSignal).toBe('SIGKILL');
|
|
610
|
-
if (command === '/bin/ps') return { status: 0, stdout: `${uid} ${pid} ${process.pid} S Dia\n`, stderr: '' };
|
|
611
|
-
return { status: 0, stdout: 'private-header\nCall graph:\n 10 Thread_1 DispatchQueue_1: com.apple.main-thread\n + 10 CFRunLoopRun (in CoreFoundation) private-path\nBinary Images:\nprivate-image', stderr: '' };
|
|
612
|
-
}) as typeof spawnSync);
|
|
613
|
-
expect(calls).toEqual([['/bin/ps', '-p', String(pid), '-o', 'uid=,pid=,ppid=,state=,ucomm='],
|
|
614
|
-
['/usr/bin/sample', String(pid), '1', '10', '-file', '/dev/stdout']]);
|
|
615
|
-
expect(result).toMatchObject({ available: true, attempted: true, ownedLiveChildConfirmed: true, reason: 'sampled',
|
|
616
|
-
waitFamilies: { mainThreadFrameCounts: { runloop: 1 } } });
|
|
617
|
-
expect(JSON.stringify(result)).not.toContain('private-');
|
|
618
|
-
expect(JSON.stringify(result)).not.toContain('CFRunLoopRun');
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
test('sampling refuses changed identity, zombies, and closed roots without attempting a native sample', () => {
|
|
622
|
-
const uid = process.getuid!();
|
|
623
|
-
const child: any = { pid: 12345, executable: '/owned/Dia', closeObserved: false, process: { pid: 12345, exitCode: null, signalCode: null } };
|
|
624
|
-
for (const row of [`${uid + 1} 12345 ${process.pid} S Dia`, `${uid} 12345 ${process.pid + 1} S Dia`,
|
|
625
|
-
`${uid} 12345 ${process.pid} Z Dia`, `${uid} 12345 ${process.pid} S other`, `${uid} 12346 ${process.pid} S Dia`]) {
|
|
626
|
-
let calls = 0;
|
|
627
|
-
const result = sampleOwnedDiaWait(child, uid, child.executable, performance.now() + 10_000, {},
|
|
628
|
-
((command: string) => { calls++; expect(command).toBe('/bin/ps'); return { status: 0, stdout: row, stderr: '' }; }) as typeof spawnSync);
|
|
629
|
-
expect(calls).toBe(1);
|
|
630
|
-
expect(result.attempted).toBe(false);
|
|
631
|
-
}
|
|
632
|
-
for (const changed of [{ ...child, closeObserved: true }, { ...child, executable: '/other/Chromium' },
|
|
633
|
-
{ ...child, process: { ...child.process, exitCode: 0 } }]) {
|
|
634
|
-
const result = sampleOwnedDiaWait(changed, uid, '/owned/Dia', performance.now() + 10_000, {}, (() => { throw new Error('must_not_spawn'); }) as typeof spawnSync);
|
|
635
|
-
expect(result).toMatchObject({ available: false, attempted: false });
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
test('sampling reports OS permission refusal without escalation or raw failure text', () => {
|
|
640
|
-
const uid = process.getuid!();
|
|
641
|
-
const child: any = { pid: 12345, executable: '/owned/Dia', closeObserved: false, process: { pid: 12345, exitCode: null, signalCode: null } };
|
|
642
|
-
const result = sampleOwnedDiaWait(child, uid, child.executable, performance.now() + 10_000, {},
|
|
643
|
-
((command: string) => command === '/bin/ps'
|
|
644
|
-
? { status: 0, stdout: `${uid} 12345 ${process.pid} S Dia`, stderr: '' }
|
|
645
|
-
: { status: 1, stdout: '', stderr: 'Failed to get task for pid: private-process-path. Operation not permitted.' }) as typeof spawnSync);
|
|
646
|
-
expect(result).toMatchObject({ available: false, attempted: true, reason: 'sampling_permission_denied' });
|
|
647
|
-
expect(JSON.stringify(result)).not.toContain('private-process');
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
test('child close joining stays within its deadline and refuses an unconfirmed close', async () => {
|
|
651
|
-
await expect(joinOwnedBrowserClose({ closed: Promise.resolve(), closeObserved: false } as any, performance.now() + 1000))
|
|
652
|
-
.rejects.toThrow('owned_child_close_unconfirmed');
|
|
653
|
-
await expect(joinOwnedBrowserClose({ closed: new Promise(() => {}), closeObserved: false } as any, performance.now() + 5))
|
|
654
|
-
.rejects.toThrow('operation_timeout');
|
|
655
|
-
await expect(joinOwnedBrowserClose({ closed: Promise.resolve(), closeObserved: true } as any, 0))
|
|
656
|
-
.rejects.toThrow('cleanup_budget_exhausted');
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
for (const phase of ['probe', 'signal']) {
|
|
660
|
-
test(`an initial ${phase} error is retained while child reaping still precedes the final absence proof`, async () => {
|
|
661
|
-
const facts: Record<string, any> = { signalSent: false, absenceConfirmed: false };
|
|
662
|
-
const child: any = { pid: 12345, closeObserved: false };
|
|
663
|
-
child.closed = Promise.resolve().then(() => { child.closeObserved = true; });
|
|
664
|
-
const calls: Array<0 | 'SIGKILL'> = [];
|
|
665
|
-
await stopOwnedBrowserGroup(child, performance.now() + 1000, facts, (_pid, signal) => {
|
|
666
|
-
calls.push(signal);
|
|
667
|
-
if (!child.closeObserved) {
|
|
668
|
-
if (phase === 'signal' && signal === 0) return;
|
|
669
|
-
throw Object.assign(new Error('private-probe-error'), { code: 'EPERM', errno: 1 });
|
|
670
|
-
}
|
|
671
|
-
throw Object.assign(new Error('gone'), { code: 'ESRCH' });
|
|
672
|
-
});
|
|
673
|
-
expect(calls).toEqual(phase === 'probe' ? [0, 0] : [0, 'SIGKILL', 0]);
|
|
674
|
-
expect(facts).toMatchObject({ stage: 'completed', signalSent: false, childCloseObserved: true, absenceConfirmed: true,
|
|
675
|
-
initialSignalFailure: { stage: phase === 'probe' ? 'probe_before_signal' : 'signal', code: 'EPERM', errno: 1 } });
|
|
676
|
-
expect(JSON.stringify(facts)).not.toContain('private-probe-error');
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
test('permission errors and zombies never replace the required child close and actual group absence', async () => {
|
|
681
|
-
const facts = { signalSent: false, absenceConfirmed: false };
|
|
682
|
-
const waiting: any = { pid: 12345, closed: new Promise(() => {}), closeObserved: false };
|
|
683
|
-
const denied = () => { throw Object.assign(new Error('denied'), { code: 'EPERM' }); };
|
|
684
|
-
await expect(stopOwnedBrowserGroup(waiting, performance.now() + 10, facts, denied)).rejects.toThrow('operation_timeout');
|
|
685
|
-
expect(facts.absenceConfirmed).toBe(false);
|
|
686
|
-
const closed: any = { pid: 12345, closed: Promise.resolve(), closeObserved: true };
|
|
687
|
-
await expect(stopOwnedBrowserGroup(closed, performance.now() + 1000, facts, denied)).rejects.toMatchObject({ code: 'EPERM' });
|
|
688
|
-
expect(facts.absenceConfirmed).toBe(false);
|
|
689
|
-
await expect(stopOwnedBrowserGroup(closed, performance.now() + 10, facts, () => {})).rejects.toThrow('owned_process_group_still_live');
|
|
690
|
-
expect(facts.absenceConfirmed).toBe(false);
|
|
691
|
-
});
|
|
692
|
-
for (const [error, category] of [
|
|
693
|
-
[new Error('browserType.launchPersistentContext: browser_launch_policy_rejected synthetic-private-value'), 'launch_policy_rejected'],
|
|
694
|
-
[new Error('background_browser_ownership_failed'), 'ownership_unconfirmed'],
|
|
695
|
-
[new Error('background_browser_startup_page_rejected'), 'startup_page_rejected'],
|
|
696
|
-
[new Error('background_browser_render_failed'), 'render_mismatch'],
|
|
697
|
-
[Object.assign(new Error('synthetic-private-value'), { name: 'TimeoutError' }), 'operation_timeout'],
|
|
698
|
-
[new Error('native_operation_timed_out'), 'operation_timeout'],
|
|
699
|
-
[new Error('operation_timeout'), 'operation_timeout'],
|
|
700
|
-
[new Error('qualification_budget_exhausted'), 'operation_timeout'],
|
|
701
|
-
[new Error('source_process_ownership_unconfirmed'), 'ownership_unconfirmed'],
|
|
702
|
-
[new Error('destination_process_ownership_unconfirmed'), 'ownership_unconfirmed'],
|
|
703
|
-
[new Error('onboarding_or_external_page'), 'startup_page_rejected'],
|
|
704
|
-
[Object.assign(new Error('synthetic-private-value'), { code: 'ENOENT' }), 'executable_unavailable'],
|
|
705
|
-
[Object.assign(new Error('synthetic-private-value'), { code: 'EACCES' }), 'permission_denied'],
|
|
706
|
-
[{ name: 'ResolveMessage', code: 'ERR_MODULE_NOT_FOUND', message: 'synthetic-private-value' }, 'module_unavailable'],
|
|
707
|
-
[Object.assign(new Error('synthetic-private-value'), { code: 'MODULE_NOT_FOUND' }), 'module_unavailable'],
|
|
708
|
-
[Object.assign(new Error('synthetic-private-value'), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' }), 'module_export_unavailable'],
|
|
709
|
-
[Object.assign(new Error('synthetic-private-value'), { code: 'ERR_REQUIRE_ESM' }), 'module_format_error'],
|
|
710
|
-
[new RangeError('synthetic-private-value'), 'invalid_runtime_range'],
|
|
711
|
-
[new TypeError('synthetic-private-value'), 'runtime_type_error'],
|
|
712
|
-
[new Error('dyld[123]: Library not loaded: synthetic-private-value'), 'dynamic_library_error'],
|
|
713
|
-
[new Error('code signature invalid: synthetic-private-value'), 'code_signing_error'],
|
|
714
|
-
[new Error('ProcessSingleton synthetic-private-value'), 'browser_profile_unavailable'],
|
|
715
|
-
[new Error('bootstrap_check_in failed synthetic-private-value'), 'graphics_or_bootstrap_error'],
|
|
716
|
-
[new Error('Target page, context or browser has been closed synthetic-private-value'), 'target_closed'],
|
|
717
|
-
[new Error('Protocol error: synthetic-private-value'), 'protocol_error'],
|
|
718
|
-
[new Error('synthetic-private-value'), 'unclassified_browser_error'],
|
|
719
|
-
[{ code: 'synthetic-private-value', message: 'synthetic-private-value' }, 'unclassified_browser_error'],
|
|
720
|
-
] as const) {
|
|
721
|
-
test(`browser diagnostics return only the allowlisted ${category} category`, () => {
|
|
722
|
-
expect(browserPreflightError(error)).toBe(category);
|
|
723
|
-
expect(browserPreflightError(error)).not.toContain('synthetic-private-value');
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
test('module-load facts retain only known error identifiers and known dependency filenames', () => {
|
|
728
|
-
const snapshot = realpathSync(mkdtempSync(path.join(root, 'module-facts-')));
|
|
729
|
-
const packageDirectory = path.join(snapshot, 'node_modules/playwright');
|
|
730
|
-
mkdirSync(packageDirectory, { recursive: true });
|
|
731
|
-
writeFileSync(path.join(packageDirectory, 'package.json'), '{}', { mode: 0o600 });
|
|
732
|
-
const facts = playwrightModuleLoadFacts(snapshot, { name: 'ResolveMessage', code: 'ERR_MODULE_NOT_FOUND',
|
|
733
|
-
message: "Cannot find package 'playwright' imported from /synthetic-private-value/worker.ts" });
|
|
734
|
-
expect(facts.errorType).toBe('ResolveMessage');
|
|
735
|
-
expect(facts.errorCode).toBe('ERR_MODULE_NOT_FOUND');
|
|
736
|
-
expect(facts.requestedModule).toBe('playwright');
|
|
737
|
-
expect(facts.files['playwright/package.json']).toEqual({ exists: true, readable: true, ownedByCurrentUid: true, insideSnapshot: true });
|
|
738
|
-
expect(facts.files['playwright-core/lib/coreBundle.js'].exists).toBe(false);
|
|
739
|
-
expect(JSON.stringify(facts)).not.toContain('synthetic-private-value');
|
|
740
|
-
expect(JSON.stringify(facts)).not.toContain(snapshot);
|
|
741
|
-
const unknown = playwrightModuleLoadFacts(snapshot, { name: 'synthetic-private-value', code: 'synthetic-private-value', message: "Cannot find package 'synthetic-private-value'" });
|
|
742
|
-
expect(unknown.errorType).toBe('unclassified');
|
|
743
|
-
expect(unknown.errorCode).toBe('unclassified');
|
|
744
|
-
expect(unknown.requestedModule).toBe('unclassified');
|
|
745
|
-
expect(JSON.stringify(unknown)).not.toContain('synthetic-private-value');
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
test('Keychain snapshots preserve exact quoted paths without shell parsing', () => {
|
|
749
|
-
expect(parseKeychainPaths(' "/Users/runner/Library/Keychains/login.keychain-db"\n "/tmp/fixture keychain.keychain-db"\n'))
|
|
750
|
-
.toEqual(['/Users/runner/Library/Keychains/login.keychain-db', '/tmp/fixture keychain.keychain-db']);
|
|
751
|
-
for (const output of ['', 'not-json', '"relative-path"', '42', '"/valid/path"\ninvalid']) {
|
|
752
|
-
expect(() => parseKeychainPaths(output)).toThrow();
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
test('fresh users can have an empty user search list and no default Keychain', () => {
|
|
757
|
-
const calls: string[][] = [];
|
|
758
|
-
const snapshot = captureUserKeychains({ HOME: root }, [root], 1000, (args, timeout) => {
|
|
759
|
-
calls.push(args);
|
|
760
|
-
expect(timeout).toBeGreaterThan(0);
|
|
761
|
-
expect(timeout).toBeLessThanOrEqual(1000);
|
|
762
|
-
return args[0] === 'list-keychains' ? { status: 0, stdout: '', stderr: '' }
|
|
763
|
-
: { status: 1, stdout: '', stderr: 'security: SecKeychainCopyDomainDefault user: A default keychain could not be found.\n' };
|
|
764
|
-
});
|
|
765
|
-
expect(snapshot).toEqual({ search: [], default: [] });
|
|
766
|
-
expect(calls).toEqual([['list-keychains', '-d', 'user'], ['default-keychain', '-d', 'user']]);
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
test('computed Keychain timeouts reach real spawnSync as bounded integer milliseconds', () => {
|
|
770
|
-
let reads = 0;
|
|
771
|
-
const times = [0.125, 0.5, 5.75];
|
|
772
|
-
const clock = spyOn(performance, 'now').mockImplementation(() => times[Math.min(reads++, times.length - 1)]);
|
|
773
|
-
const timeouts: number[] = [];
|
|
774
|
-
const calls: string[][] = [];
|
|
775
|
-
try {
|
|
776
|
-
const snapshot = captureUserKeychains({ HOME: root }, [root], 10_000, (args, timeout) => {
|
|
777
|
-
const result = spawnSync(process.execPath, ['--no-env-file', '--no-install', '--no-macros',
|
|
778
|
-
`--config=${process.platform === 'win32' ? 'NUL' : '/dev/null'}`, '-e', 'process.exit(0)'], {
|
|
779
|
-
cwd: root, env: { HOME: root, PATH: path.dirname(process.execPath) }, encoding: 'utf8', timeout,
|
|
780
|
-
});
|
|
781
|
-
expect(result.status).toBe(0);
|
|
782
|
-
expect(result.error).toBeUndefined();
|
|
783
|
-
timeouts.push(timeout);
|
|
784
|
-
calls.push(args);
|
|
785
|
-
return result;
|
|
786
|
-
});
|
|
787
|
-
expect(snapshot).toEqual({ search: [], default: [] });
|
|
788
|
-
expect(timeouts).toEqual([9999, 9994]);
|
|
789
|
-
expect(calls).toEqual([['list-keychains', '-d', 'user'], ['default-keychain', '-d', 'user']]);
|
|
790
|
-
} finally {
|
|
791
|
-
clock.mockRestore();
|
|
792
|
-
}
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
test('an expired or sub-millisecond Keychain budget never becomes an unbounded subprocess', () => {
|
|
796
|
-
const clock = spyOn(performance, 'now').mockReturnValue(100);
|
|
797
|
-
let commands = 0;
|
|
798
|
-
try {
|
|
799
|
-
for (const budget of [0.75, 0, -1, NaN, Infinity]) {
|
|
800
|
-
expect(() => captureUserKeychains({ HOME: root }, [root], budget, () => {
|
|
801
|
-
commands++;
|
|
802
|
-
return { status: 0, stdout: '', stderr: '' };
|
|
803
|
-
})).toThrow('user_keychain_probe_timeout');
|
|
804
|
-
}
|
|
805
|
-
expect(commands).toBe(0);
|
|
806
|
-
} finally {
|
|
807
|
-
clock.mockRestore();
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
test('permission, securityd, and transport errors are never mistaken for no default Keychain', () => {
|
|
812
|
-
for (const result of [
|
|
813
|
-
{ status: 1, stdout: '', stderr: 'security: SecKeychainCopyDefault: User interaction is not allowed.' },
|
|
814
|
-
{ status: 1, stdout: '', stderr: 'security: SecKeychainCopyDomainDefault system: A default keychain could not be found.' },
|
|
815
|
-
{ status: 1, stdout: '', stderr: 'synthetic-private-error' },
|
|
816
|
-
{ status: 0, stdout: '', stderr: 'synthetic-private-error' },
|
|
817
|
-
{ status: 1, stdout: 'unexpected-data', stderr: 'security: SecKeychainCopyDefault: A default keychain could not be found.' },
|
|
818
|
-
{ status: null, stdout: '', stderr: '', error: new Error('synthetic-private-error') },
|
|
819
|
-
]) expect(() => parseDefaultKeychain(result)).toThrow('user_default_keychain_unavailable');
|
|
820
|
-
expect(parseDefaultKeychain({ status: 0, stdout: '', stderr: '' })).toEqual([]);
|
|
821
|
-
expect(parseDefaultKeychain({ status: 1, stdout: '', stderr: 'security: SecKeychainCopyDefault: A default keychain could not be found.' })).toEqual([]);
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
test('the snapshot boundary rejects System Keychain fallback and paths outside the owned home', () => {
|
|
825
|
-
for (const file of ['/Library/Keychains/System.keychain', '/System/Library/Keychains/SystemRootCertificates.keychain', '/unowned/keychain']) {
|
|
826
|
-
expect(() => captureUserKeychains({ HOME: root }, [root], 1000, args => ({ status: 0,
|
|
827
|
-
stdout: args[0] === 'list-keychains' ? JSON.stringify(file) : '', stderr: '' })))
|
|
828
|
-
.toThrow('keychain_outside_owned_home_refused');
|
|
829
|
-
}
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
test('a fresh Keychain home gains only owned standard directories, not a fabricated preference file', () => {
|
|
833
|
-
const home = realpathSync(mkdtempSync(path.join(root, 'keychain-home-')));
|
|
834
|
-
expect(prepareKeychainHome(home)).toEqual({ before: { Library: false, 'Library/Preferences': false, 'Library/Keychains': false }, directoriesReady: true });
|
|
835
|
-
for (const directory of ['Library', 'Library/Preferences', 'Library/Keychains']) {
|
|
836
|
-
expect(lstatSync(path.join(home, directory)).uid).toBe(process.getuid!());
|
|
837
|
-
expect(lstatSync(path.join(home, directory)).isDirectory()).toBe(true);
|
|
838
|
-
}
|
|
839
|
-
const preferences = path.join(home, 'Library/Preferences/com.apple.security.plist');
|
|
840
|
-
expect(existsSync(preferences)).toBe(false);
|
|
841
|
-
expect(existsSync(path.join(home, 'Library/Safari'))).toBe(false);
|
|
842
|
-
writeFileSync(preferences, 'opaque fixture preferences', { mode: 0o600 });
|
|
843
|
-
expect(prepareKeychainHome(home).before).toEqual({ Library: true, 'Library/Preferences': true, 'Library/Keychains': true });
|
|
844
|
-
expect(readFileSync(preferences, 'utf8')).toBe('opaque fixture preferences');
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
test('home preparation rejects a linked or unowned home without writing through it', () => {
|
|
848
|
-
const home = realpathSync(mkdtempSync(path.join(root, 'linked-home-')));
|
|
849
|
-
const elsewhere = realpathSync(mkdtempSync(path.join(root, 'other-home-')));
|
|
850
|
-
symlinkSync(elsewhere, path.join(home, 'Library'), 'dir');
|
|
851
|
-
expect(() => prepareKeychainHome(home)).toThrow('keychain_home_unsafe');
|
|
852
|
-
expect(readdirSync(elsewhere)).toEqual([]);
|
|
853
|
-
expect(() => prepareKeychainHome(elsewhere, process.getuid!() + 1)).toThrow('keychain_home_unsafe');
|
|
854
|
-
expect(readdirSync(elsewhere)).toEqual([]);
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
for (const mismatch of ['search-empty', 'default-empty', 'search-path', 'default-path', 'search-error', 'default-error', 'read-mismatch', 'read-error']) {
|
|
858
|
-
test(`Keychain diagnostics observe the explicit read independently of ${mismatch}`, () => {
|
|
859
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'keychain-facts-')));
|
|
860
|
-
const keychain = path.join(directory, 'fixture.keychain-db');
|
|
861
|
-
const other = path.join(directory, 'other.keychain-db');
|
|
862
|
-
writeFileSync(keychain, 'opaque synthetic keychain', { mode: 0o600 });
|
|
863
|
-
writeFileSync(other, 'other synthetic keychain', { mode: 0o600 });
|
|
864
|
-
const expected = 'synthetic-private-equality-value';
|
|
865
|
-
const calls: string[][] = [];
|
|
866
|
-
const facts = observeFixtureKeychain({ HOME: directory }, [directory], keychain, expected, 1000, (args, timeout) => {
|
|
867
|
-
expect(Number.isInteger(timeout)).toBe(true);
|
|
868
|
-
expect(timeout).toBeGreaterThan(0);
|
|
869
|
-
calls.push(args);
|
|
870
|
-
if (args[0] === 'list-keychains') return mismatch === 'search-error'
|
|
871
|
-
? { status: 1, stdout: '', stderr: 'synthetic-private-error' }
|
|
872
|
-
: { status: 0, stdout: mismatch === 'search-empty' ? '' : JSON.stringify(mismatch === 'search-path' ? other : keychain), stderr: '' };
|
|
873
|
-
if (args[0] === 'default-keychain') return mismatch === 'default-error'
|
|
874
|
-
? { status: 1, stdout: '', stderr: 'synthetic-private-error' }
|
|
875
|
-
: mismatch === 'default-empty'
|
|
876
|
-
? { status: 1, stdout: '', stderr: 'security: SecKeychainCopyDomainDefault user: A default keychain could not be found.' }
|
|
877
|
-
: { status: 0, stdout: JSON.stringify(mismatch === 'default-path' ? other : keychain), stderr: '' };
|
|
878
|
-
if (mismatch === 'read-error') return { status: 1, stdout: '', stderr: 'synthetic-private-error' };
|
|
879
|
-
return { status: 0, stdout: mismatch === 'read-mismatch' ? 'other synthetic value' : expected + '\n', stderr: '' };
|
|
880
|
-
});
|
|
881
|
-
expect(calls).toEqual([['list-keychains', '-d', 'user'], ['default-keychain', '-d', 'user'],
|
|
882
|
-
['find-generic-password', '-s', 'Gstack Native Probe', '-w', keychain]]);
|
|
883
|
-
expect(facts.searchCount).toBe(mismatch === 'search-error' ? null : mismatch === 'search-empty' ? 0 : 1);
|
|
884
|
-
expect(facts.searchPathMatches).toBe(!mismatch.startsWith('search-'));
|
|
885
|
-
expect(facts.defaultCount).toBe(mismatch === 'default-error' ? null : mismatch === 'default-empty' ? 0 : 1);
|
|
886
|
-
expect(facts.defaultPathMatches).toBe(!mismatch.startsWith('default-'));
|
|
887
|
-
expect(facts.explicitReadAttempted).toBe(true);
|
|
888
|
-
expect(facts.explicitReadSucceeded).toBe(mismatch !== 'read-error');
|
|
889
|
-
expect(facts.explicitReadMatches).toBe(!mismatch.startsWith('read-'));
|
|
890
|
-
expect(JSON.stringify(facts)).not.toContain(expected);
|
|
891
|
-
expect(JSON.stringify(facts)).not.toContain('synthetic-private-error');
|
|
892
|
-
expect(JSON.stringify(facts)).not.toContain(keychain);
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
test('Dia Keychain diagnostics distinguish both HOME environments without configuring the shadow home', () => {
|
|
897
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'dual-keychain-')));
|
|
898
|
-
const registered = path.join(directory, 'registered');
|
|
899
|
-
const shadow = path.join(directory, 'shadow');
|
|
900
|
-
mkdirSync(registered);
|
|
901
|
-
mkdirSync(shadow);
|
|
902
|
-
prepareKeychainHome(registered);
|
|
903
|
-
writeFileSync(path.join(registered, 'Library/Preferences/com.apple.security.plist'), 'opaque private fixture preference');
|
|
904
|
-
const keychain = path.join(directory, 'fixture.keychain-db');
|
|
905
|
-
writeFileSync(keychain, 'opaque synthetic keychain', { mode: 0o600 });
|
|
906
|
-
const expected = 'synthetic-private-dual-home-value';
|
|
907
|
-
const environments = { keychainHome: { HOME: registered }, profileHome: { HOME: shadow } };
|
|
908
|
-
const calls: Array<{ home: string; args: string[] }> = [];
|
|
909
|
-
const observed = observeDiaKeychainEnvironments(environments, [directory], keychain, expected, 20_000, (env, args, timeout) => {
|
|
910
|
-
expect(Number.isInteger(timeout)).toBe(true);
|
|
911
|
-
expect(timeout).toBeGreaterThan(0);
|
|
912
|
-
expect(timeout).toBeLessThanOrEqual(10_000);
|
|
913
|
-
calls.push({ home: env.HOME, args });
|
|
914
|
-
if (args[0] === 'list-keychains') return { status: 0, stdout: env.HOME === registered ? JSON.stringify(keychain) : '', stderr: '' };
|
|
915
|
-
if (args[0] === 'default-keychain') return env.HOME === registered
|
|
916
|
-
? { status: 0, stdout: JSON.stringify(keychain), stderr: '' }
|
|
917
|
-
: { status: 1, stdout: '', stderr: 'security: SecKeychainCopyDomainDefault user: A default keychain could not be found.' };
|
|
918
|
-
expect(args.slice(0, 4)).toEqual(['find-generic-password', '-s', 'Dia Safe Storage', '-w']);
|
|
919
|
-
return { status: 0, stdout: expected, stderr: '' };
|
|
920
|
-
});
|
|
921
|
-
expect(observed.firstFailure).toEqual({ environment: 'profileHome', check: 'search_path' });
|
|
922
|
-
expect(observed.environments.keychainHome).toMatchObject({ searchCount: 1, searchPathMatches: true, defaultCount: 1, defaultPathMatches: true,
|
|
923
|
-
explicitReadMatches: true, lookupReadAttempted: true, lookupReadMatches: true, preferencesDirectoryExists: true, preferencesFileExists: true });
|
|
924
|
-
expect(observed.environments.profileHome).toMatchObject({ searchCount: 0, searchPathMatches: false, defaultCount: 0, defaultPathMatches: false,
|
|
925
|
-
explicitReadMatches: true, lookupReadAttempted: false, preferencesDirectoryExists: false, preferencesFileExists: false });
|
|
926
|
-
expect(calls).toHaveLength(7);
|
|
927
|
-
expect(calls.filter(call => call.home === shadow)).toHaveLength(3);
|
|
928
|
-
expect(calls.some(call => call.args[0] === 'default-keychain' && call.args.includes('-s'))).toBe(false);
|
|
929
|
-
expect(existsSync(path.join(shadow, 'Library'))).toBe(false);
|
|
930
|
-
const serialized = JSON.stringify(observed);
|
|
931
|
-
for (const privateValue of [expected, registered, shadow, keychain, 'opaque private']) expect(serialized).not.toContain(privateValue);
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
for (const failure of ['search', 'default', 'explicit_read']) {
|
|
935
|
-
test(`Dia diagnostic refuses unqualified lookup when the owned ${failure} proof fails`, () => {
|
|
936
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'guarded-keychain-')));
|
|
937
|
-
const keychain = path.join(directory, 'fixture.keychain-db');
|
|
938
|
-
writeFileSync(keychain, 'opaque synthetic keychain', { mode: 0o600 });
|
|
939
|
-
let implicitCalls = 0;
|
|
940
|
-
const result = observeFixtureKeychain({ HOME: directory }, [directory], keychain, 'expected-private-value', 1000, args => {
|
|
941
|
-
if (args[0] === 'list-keychains') return { status: 0, stdout: failure === 'search' ? '' : JSON.stringify(keychain), stderr: '' };
|
|
942
|
-
if (args[0] === 'default-keychain') return { status: 0, stdout: failure === 'default' ? '' : JSON.stringify(keychain), stderr: '' };
|
|
943
|
-
if (args.length === 4) implicitCalls++;
|
|
944
|
-
return { status: 0, stdout: failure === 'explicit_read' ? 'wrong-private-value' : 'expected-private-value', stderr: '' };
|
|
945
|
-
}, 'Dia Safe Storage');
|
|
946
|
-
expect(result.explicitReadAttempted).toBe(true);
|
|
947
|
-
expect(result.lookupReadAttempted).toBe(false);
|
|
948
|
-
expect(implicitCalls).toBe(0);
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
test('both Keychain environments remain observable while the first original validation failure is retained', () => {
|
|
953
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'keychain-failures-')));
|
|
954
|
-
const keychain = path.join(directory, 'fixture.keychain-db');
|
|
955
|
-
writeFileSync(keychain, 'opaque synthetic keychain', { mode: 0o600 });
|
|
956
|
-
const environments = { keychainHome: { HOME: directory }, profileHome: { HOME: directory, MARKER: 'shadow' } };
|
|
957
|
-
for (const keychainSearchFails of [true, false]) {
|
|
958
|
-
const homesObserved = new Set<Record<string, string>>();
|
|
959
|
-
const observed = observeDiaKeychainEnvironments(environments, [directory], keychain, 'expected-private-value', 20_000, (env, args) => {
|
|
960
|
-
homesObserved.add(env);
|
|
961
|
-
if (args[0] === 'list-keychains') return { status: 0,
|
|
962
|
-
stdout: env === environments.profileHome || keychainSearchFails ? '' : JSON.stringify(keychain), stderr: '' };
|
|
963
|
-
if (args[0] === 'default-keychain') return { status: 1, stdout: '', stderr: 'private-default-error' };
|
|
964
|
-
return { status: 0, stdout: 'wrong-private-value', stderr: '' };
|
|
965
|
-
});
|
|
966
|
-
expect(homesObserved.size).toBe(2);
|
|
967
|
-
expect(observed.firstFailure).toEqual({ environment: keychainSearchFails ? 'keychainHome' : 'profileHome', check: 'search_path' });
|
|
968
|
-
expect(observed.environments.keychainHome).toMatchObject({ defaultCount: null, explicitReadAttempted: true, explicitReadMatches: false });
|
|
969
|
-
expect(observed.environments.profileHome).toMatchObject({ defaultCount: null, explicitReadAttempted: true, explicitReadMatches: false });
|
|
970
|
-
expect(JSON.stringify(observed)).not.toContain('private-default-error');
|
|
971
|
-
expect(JSON.stringify(observed)).not.toContain('wrong-private-value');
|
|
972
|
-
}
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
test('Dia environment validation preserves successful and explicit-read-only outcomes', () => {
|
|
976
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'keychain-outcomes-')));
|
|
977
|
-
const keychain = path.join(directory, 'fixture.keychain-db');
|
|
978
|
-
writeFileSync(keychain, 'opaque synthetic keychain', { mode: 0o600 });
|
|
979
|
-
const environments = { keychainHome: { HOME: directory }, profileHome: { HOME: directory } };
|
|
980
|
-
for (const readMismatch of [false, true]) {
|
|
981
|
-
const observed = observeDiaKeychainEnvironments(environments, [directory], keychain, 'expected-private-value', 20_000, (env, args) => {
|
|
982
|
-
if (args[0] === 'list-keychains' || args[0] === 'default-keychain') return { status: 0, stdout: JSON.stringify(keychain), stderr: '' };
|
|
983
|
-
return { status: 0, stdout: readMismatch && env === environments.keychainHome ? 'wrong-private-value' : 'expected-private-value', stderr: '' };
|
|
984
|
-
});
|
|
985
|
-
if (readMismatch) expect(observed.firstFailure).toEqual({ environment: 'keychainHome', check: 'explicit_read' });
|
|
986
|
-
else expect(observed.firstFailure).toBeUndefined();
|
|
987
|
-
expect(observed.environments.profileHome).toMatchObject({ searchPathMatches: true, defaultPathMatches: true, explicitReadMatches: true,
|
|
988
|
-
lookupReadAttempted: true, lookupReadMatches: true });
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
test('an exhausted dual-environment diagnostic budget launches no Keychain commands', () => {
|
|
993
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'keychain-budget-')));
|
|
994
|
-
const keychain = path.join(directory, 'fixture.keychain-db');
|
|
995
|
-
writeFileSync(keychain, 'opaque synthetic keychain', { mode: 0o600 });
|
|
996
|
-
let calls = 0;
|
|
997
|
-
const observed = observeDiaKeychainEnvironments({ keychainHome: { HOME: directory }, profileHome: { HOME: directory } },
|
|
998
|
-
[directory], keychain, 'expected-private-value', 0, () => { calls++; throw new Error('must_not_spawn'); });
|
|
999
|
-
expect(calls).toBe(0);
|
|
1000
|
-
expect(observed.environments.keychainHome).toMatchObject({ searchCount: null, defaultCount: null, explicitReadAttempted: false, lookupReadAttempted: false });
|
|
1001
|
-
expect(observed.environments.profileHome).toMatchObject({ searchCount: null, defaultCount: null, explicitReadAttempted: false, lookupReadAttempted: false });
|
|
1002
|
-
expect(observed.firstFailure).toEqual({ environment: 'keychainHome', check: 'search_path' });
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
test('an originally absent default is restored by deleting only the created fixture, never by a null default setter', () => {
|
|
1006
|
-
expect(fixtureKeychainRestoreCommands({ search: [], default: [] }, '/owned/fixture.keychain-db', true)).toEqual([
|
|
1007
|
-
['delete-keychain', '/owned/fixture.keychain-db'], ['list-keychains', '-d', 'user', '-s'],
|
|
1008
|
-
]);
|
|
1009
|
-
expect(fixtureKeychainRestoreCommands({ search: [], default: [] }, '/owned/fixture.keychain-db', false)).toEqual([
|
|
1010
|
-
['list-keychains', '-d', 'user', '-s'],
|
|
1011
|
-
]);
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
test('an existing default is restored before deleting the fixture, and preexisting fixture references are refused', () => {
|
|
1015
|
-
const before = { search: ['/owned/prior.keychain-db', '/owned/other.keychain-db'], default: ['/owned/prior.keychain-db'] };
|
|
1016
|
-
expect(fixtureKeychainRestoreCommands(before, '/owned/fixture.keychain-db', true)).toEqual([
|
|
1017
|
-
['default-keychain', '-d', 'user', '-s', '/owned/prior.keychain-db'],
|
|
1018
|
-
['delete-keychain', '/owned/fixture.keychain-db'],
|
|
1019
|
-
['list-keychains', '-d', 'user', '-s', '/owned/prior.keychain-db', '/owned/other.keychain-db'],
|
|
1020
|
-
]);
|
|
1021
|
-
expect(() => fixtureKeychainRestoreCommands(before, '/owned/prior.keychain-db', true)).toThrow('fixture_keychain_not_fresh');
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
|
-
test('fresh account cleanup requires the same GUID, UID, private group, and registered home', () => {
|
|
1025
|
-
const identity = { guid: 'A38AC39B-5960-4F0C-B02F-C32A4F625B33', uid: 23456, gid: 23456, home: '/private/tmp/fixture/home' };
|
|
1026
|
-
const record = parseDirectoryRecord(`GeneratedUID: ${identity.guid}\nUniqueID: ${identity.uid}\nPrimaryGroupID: ${identity.gid}\nNFSHomeDirectory: ${identity.home}\n`);
|
|
1027
|
-
expect(ownsFreshAccount(record, identity)).toBe(true);
|
|
1028
|
-
for (const key of ['GeneratedUID', 'UniqueID', 'PrimaryGroupID', 'NFSHomeDirectory']) {
|
|
1029
|
-
expect(ownsFreshAccount({ ...record, [key]: 'different' }, identity)).toBe(false);
|
|
1030
|
-
}
|
|
1031
|
-
expect(() => parseDirectoryRecord('UniqueID: 23456\nUniqueID: 501')).toThrow('invalid_directory_record');
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
test('user-domain absence requires an explicit matching-domain response, not an arbitrary command failure', () => {
|
|
1035
|
-
for (const status of [64, 113]) {
|
|
1036
|
-
const absent = classifyUserDomain(23456, { status, stdout: '', stderr: 'Bad request.\nCould not find domain for user uid: 23456\n' });
|
|
1037
|
-
expect(absent.state).toBe('absent');
|
|
1038
|
-
expect(absent.uid).toBe(23456);
|
|
1039
|
-
}
|
|
1040
|
-
for (const result of [
|
|
1041
|
-
{ status: 1, stdout: '', stderr: 'sudo: a password is required' },
|
|
1042
|
-
{ status: 113, stdout: '', stderr: 'Could not find domain for user uid: 23457' },
|
|
1043
|
-
{ status: 113, stdout: '', stderr: 'Could not find domain for user gui: 23456' },
|
|
1044
|
-
{ status: 113, stdout: '', stderr: 'synthetic-private-error' },
|
|
1045
|
-
{ status: null, stdout: '', stderr: '', error: new Error('synthetic-private-error') },
|
|
1046
|
-
]) expect(classifyUserDomain(23456, result).state).toBe('unavailable');
|
|
1047
|
-
expect(() => classifyUserDomain(0, { status: 0, stdout: '', stderr: '' })).toThrow('invalid_fresh_user_domain');
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
test('user-domain inspection records only safe state facts and notices an unexpected GUI domain', () => {
|
|
1051
|
-
const present = classifyUserDomain(23456, { status: 0, stdout: 'user/23456 = {\n type = user\n synthetic-private-value\n}', stderr: '' });
|
|
1052
|
-
expect(present).toMatchObject({ uid: 23456, state: 'present', hasGuiDomain: false, exitCode: 0 });
|
|
1053
|
-
expect(JSON.stringify(present)).not.toContain('synthetic-private-value');
|
|
1054
|
-
expect(classifyUserDomain(23456, { status: 0, stdout: 'user/23456 = {\n subdomains = { gui/23456 }\n}', stderr: '' }).hasGuiDomain).toBe(true);
|
|
1055
|
-
expect(classifyUserDomain(23456, { status: 0, stdout: 'user/23456 = {\n session = Aqua\n}', stderr: '' }).hasGuiDomain).toBe(true);
|
|
1056
|
-
expect(classifyUserDomain(23456, { status: 0, stdout: 'user/501 = { }', stderr: '' }).state).toBe('unavailable');
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
test('domain structure retains counts and creator correlation, never arbitrary labels or environment values', () => {
|
|
1060
|
-
const stdout = `user/23456 = {
|
|
1061
|
-
\ttype = user
|
|
1062
|
-
\thandle = 23456
|
|
1063
|
-
\tactive count = 3
|
|
1064
|
-
\ton-demand count = 0
|
|
1065
|
-
\tservice count = 2
|
|
1066
|
-
\tactive service count = 1
|
|
1067
|
-
\texternal activation count = 0
|
|
1068
|
-
\tin-progress bootstraps = 0
|
|
1069
|
-
\tpended requests = 0
|
|
1070
|
-
\tcreator = launchctl.4567
|
|
1071
|
-
\tcreator euid = 0
|
|
1072
|
-
\tenvironment = {
|
|
1073
|
-
\t\tsynthetic-private-variable => synthetic-private-value
|
|
1074
|
-
\t\tservice count = 999
|
|
1075
|
-
\t}
|
|
1076
|
-
\tservices = {
|
|
1077
|
-
\t\t234 0 synthetic-private-service
|
|
1078
|
-
\t\t0 0 synthetic-private-job
|
|
1079
|
-
\t}
|
|
1080
|
-
\tsubdomains = {
|
|
1081
|
-
\t\tsynthetic-private-child
|
|
1082
|
-
\t}
|
|
1083
|
-
\tunmanaged processes = {
|
|
1084
|
-
\t}
|
|
1085
|
-
\tendpoints = {}
|
|
1086
|
-
}`;
|
|
1087
|
-
const observation = classifyUserDomain(23456, { status: 0, stdout, stderr: '' }, 4567);
|
|
1088
|
-
expect(observation.structure).toMatchObject({ complete: true, type: 'user', handleMatchesUid: true, creator: 'launchctl', creatorIsProbe: true,
|
|
1089
|
-
counts: { 'service count': 2, 'active service count': 1, 'creator euid': 0, 'in-progress bootstraps': 0 },
|
|
1090
|
-
sectionNonemptyLines: { services: 2, subdomains: 1, 'unmanaged processes': 0, endpoints: 0, jobs: null } });
|
|
1091
|
-
expect(JSON.stringify(observation)).not.toContain('synthetic-private');
|
|
1092
|
-
expect(JSON.stringify(observation)).not.toContain('4567');
|
|
1093
|
-
expect(classifyUserDomain(23456, { status: 0, stdout, stderr: '' }, 4568).structure?.creatorIsProbe).toBe(false);
|
|
1094
|
-
expect(classifyUserDomain(23456, { status: 0, stdout, stderr: '' }).structure?.creatorIsProbe).toBeNull();
|
|
1095
|
-
expect(classifyUserDomain(23456, { status: 0, stdout: stdout.replace('launchctl.4567', 'synthetic-private-creator.4567'), stderr: '' }, 4567).structure?.creator).toBe('other');
|
|
1096
|
-
});
|
|
1097
|
-
|
|
1098
|
-
test('missing, malformed, duplicate, and incomplete domain facts never become empty-baseline evidence', () => {
|
|
1099
|
-
const minimal = 'user/23456 = {\n\ttype = user\n\thandle = 23456\n}';
|
|
1100
|
-
const observation = classifyUserDomain(23456, { status: 0, stdout: minimal, stderr: '' });
|
|
1101
|
-
expect(observation.structure?.counts['service count']).toBeNull();
|
|
1102
|
-
expect(observation.structure?.sectionNonemptyLines.services).toBeNull();
|
|
1103
|
-
expect(observation.structure?.creatorIsProbe).toBeNull();
|
|
1104
|
-
for (const stdout of [minimal.slice(0, -1), minimal.replace('\thandle', '\ttype = user\n\thandle'),
|
|
1105
|
-
minimal.replace('\thandle = 23456', '\tservices = {\n\t\tsynthetic-private-service')]) {
|
|
1106
|
-
expect(classifyUserDomain(23456, { status: 0, stdout, stderr: '' }).structure?.complete).toBe(false);
|
|
1107
|
-
}
|
|
1108
|
-
for (const value of ['-1', 'NaN', '1.5', '9007199254740992', 'synthetic-private-value']) {
|
|
1109
|
-
const stdout = minimal.replace('\thandle', '\tservice count = ' + value + '\n\thandle');
|
|
1110
|
-
expect(classifyUserDomain(23456, { status: 0, stdout, stderr: '' }).structure?.counts['service count']).toBeNull();
|
|
1111
|
-
}
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
|
-
test('known bracketed creator syntax is matched exactly while unsupported spellings remain unknown', () => {
|
|
1115
|
-
const fixture = (creator: string) => classifyUserDomain(23456, { status: 0,
|
|
1116
|
-
stdout: `user/23456 = {\n\ttype = user\n\tcreator = ${creator}\n}`, stderr: '' }, 4567);
|
|
1117
|
-
expect(fixture('launchctl[4567]').structure?.creatorIsProbe).toBe(true);
|
|
1118
|
-
expect(fixture('launchctl[4568]').structure?.creatorIsProbe).toBe(false);
|
|
1119
|
-
for (const creator of ['launchctl(4567)', 'launchctl[4567] extra', 'other[4567]', 'launchctl[0]', 'launchctl[4567)']) {
|
|
1120
|
-
expect(fixture(creator).structure?.creatorIsProbe).toBeNull();
|
|
1121
|
-
expect(fixture(creator).structure?.creator).toBe('other');
|
|
1122
|
-
}
|
|
1123
|
-
});
|
|
1124
|
-
|
|
1125
|
-
test('parent-domain observation extracts only candidate UID membership from complete subdomains', () => {
|
|
1126
|
-
for (const [header, target, gui] of [
|
|
1127
|
-
['system', 'user/23456', 'gui/23456'],
|
|
1128
|
-
['com.apple.xpc.launchd.domain.system', 'com.apple.xpc.launchd.domain.user.23456', 'com.apple.xpc.launchd.user.domain.23456.100007.Aqua'],
|
|
1129
|
-
]) {
|
|
1130
|
-
const stdout = `${header} = {\n\ttype = system\n\tsubdomains = {\n\t\t${target}\n\t\t${gui}\n\t\tpid/15\n\t}\n}`;
|
|
1131
|
-
expect(classifyParentDomain(23456, { status: 0, stdout, stderr: '' })).toMatchObject({ state: 'present', parseStage: 'parsed',
|
|
1132
|
-
subdomainCount: 3, matchingUserDomains: 1, matchingGuiDomains: 1, unrecognizedEntries: 0 });
|
|
1133
|
-
}
|
|
1134
|
-
const stdout = `system = {
|
|
1135
|
-
\ttype = system
|
|
1136
|
-
\tenvironment = {
|
|
1137
|
-
\t\tsynthetic-private-name => user/23456
|
|
1138
|
-
\t\tsubdomains = {
|
|
1139
|
-
\t\t\tuser/23456
|
|
1140
|
-
\t\t}
|
|
1141
|
-
\t}
|
|
1142
|
-
\tsubdomains = {
|
|
1143
|
-
\t\tuser/234560
|
|
1144
|
-
\t\tgui/501
|
|
1145
|
-
\t\tpid/123
|
|
1146
|
-
\t\tsession/100007
|
|
1147
|
-
\t\tcom.apple.xpc.launchd.domain.pid.synthetic-private-process.23456
|
|
1148
|
-
\t}
|
|
1149
|
-
\tservices = {
|
|
1150
|
-
\t\tsynthetic-private-service-user/23456
|
|
1151
|
-
\t}
|
|
1152
|
-
}`;
|
|
1153
|
-
const observation = classifyParentDomain(23456, { status: 0, stdout, stderr: '' });
|
|
1154
|
-
expect(observation).toMatchObject({ state: 'absent', parseStage: 'parsed', subdomainCount: 5, matchingUserDomains: 0 });
|
|
1155
|
-
expect(JSON.stringify(observation)).not.toContain('synthetic-private');
|
|
1156
|
-
expect(JSON.stringify(observation)).not.toContain('234560');
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
test('parent-domain absence refuses incomplete, missing, duplicate, nested-only, or unknown subdomain output', () => {
|
|
1160
|
-
const empty = 'system = {\n\ttype = system\n\tsubdomains = {}\n}';
|
|
1161
|
-
expect(classifyParentDomain(23456, { status: 0, stdout: empty, stderr: '' }).state).toBe('absent');
|
|
1162
|
-
for (const stdout of [empty.slice(0, -1), empty.replace('system = {', 'user/23456 = {'),
|
|
1163
|
-
empty.replace('\tsubdomains = {}\n', ''), empty.replace('\tsubdomains = {}', '\tsubdomains = {}\n\tsubdomains = {}'),
|
|
1164
|
-
empty.replace('type = system', 'type = user'),
|
|
1165
|
-
empty.replace('\tsubdomains = {}', '\tenvironment = {\n\t\tsubdomains = {}\n\t}'),
|
|
1166
|
-
empty.replace('subdomains = {}', 'subdomains = {\n\t\tunknown-private-domain\n\t}'),
|
|
1167
|
-
empty.replace('subdomains = {}', 'subdomains = {\n\t\tuser/23456\n\t\tunknown-private-domain\n\t}')]) {
|
|
1168
|
-
const observation = classifyParentDomain(23456, { status: 0, stdout, stderr: '' });
|
|
1169
|
-
expect(observation.state).toBe('unavailable');
|
|
1170
|
-
expect(JSON.stringify(observation)).not.toContain('private-domain');
|
|
1171
|
-
}
|
|
1172
|
-
expect(classifyParentDomain(23456, { status: 1, stdout: empty, stderr: 'synthetic-private-error' }).state).toBe('unavailable');
|
|
1173
|
-
expect(classifyParentDomain(23456, { status: 0, stdout: empty, stderr: '', error: new Error('synthetic-private-error') }).state).toBe('unavailable');
|
|
1174
|
-
expect(classifyParentDomain(23456, { status: 0, stdout: empty + ' '.repeat(1024 * 1024), stderr: '' }).parseStage).toBe('oversized');
|
|
1175
|
-
});
|
|
1176
|
-
|
|
1177
|
-
test('the registered parent observer queries only the existing system domain under its original bounds', () => {
|
|
1178
|
-
const env = { HOME: root, PATH: '/usr/bin:/bin' };
|
|
1179
|
-
const observation = inspectParentDomain(23456, performance.now() + 10_000, env,
|
|
1180
|
-
((command: string, args: string[], options: any) => {
|
|
1181
|
-
expect(command).toBe('/usr/bin/sudo');
|
|
1182
|
-
expect(args).toEqual(['-n', '/bin/launchctl', 'print', 'system']);
|
|
1183
|
-
expect(options.env).toBe(env);
|
|
1184
|
-
expect(options.maxBuffer).toBe(1024 * 1024);
|
|
1185
|
-
expect(Number.isInteger(options.timeout)).toBe(true);
|
|
1186
|
-
expect(options.timeout).toBeGreaterThan(0);
|
|
1187
|
-
expect(options.timeout).toBeLessThanOrEqual(3_000);
|
|
1188
|
-
return { status: 0, stdout: 'system = {\n\ttype = system\n\tsubdomains = {}\n}', stderr: '' };
|
|
1189
|
-
}) as typeof spawnSync);
|
|
1190
|
-
expect(observation.state).toBe('absent');
|
|
1191
|
-
const never = (() => { throw new Error('must_not_spawn'); }) as typeof spawnSync;
|
|
1192
|
-
expect(() => inspectParentDomain(501, performance.now() + 10_000, {}, never)).toThrow('invalid_fresh_user_domain');
|
|
1193
|
-
for (const deadline of [0, NaN, Infinity]) expect(() => inspectParentDomain(23456, deadline, {}, never)).toThrow('fresh_launcher_deadline');
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
test('the registered UID observer filters numeric global ps rows without resolving an unregistered account', () => {
|
|
1197
|
-
const env = { HOME: root, PATH: '/usr/bin:/bin' };
|
|
1198
|
-
const observation = inspectUidProcesses(23456, performance.now() + 10_000, env,
|
|
1199
|
-
((command: string, args: string[], options: any) => {
|
|
1200
|
-
expect(command).toBe('/bin/ps');
|
|
1201
|
-
expect(args).toEqual(['-axo', 'uid=,pid=,ppid=,state=,ucomm=']);
|
|
1202
|
-
expect(options.env).toBe(env);
|
|
1203
|
-
expect(options.maxBuffer).toBe(128 * 1024);
|
|
1204
|
-
expect(Number.isInteger(options.timeout)).toBe(true);
|
|
1205
|
-
expect(options.timeout).toBeGreaterThan(0);
|
|
1206
|
-
expect(options.timeout).toBeLessThanOrEqual(2_000);
|
|
1207
|
-
return { status: 0, stdout: '501 300 1 S synthetic-private-name\n23456 400 1 S distnoted\n', stderr: '' };
|
|
1208
|
-
}) as typeof spawnSync);
|
|
1209
|
-
expect(observation).toMatchObject({ available: true, count: 1, processes: [{ pid: 400, ppid: 1, state: 'S', basename: 'distnoted' }] });
|
|
1210
|
-
expect(JSON.stringify(observation)).not.toContain('synthetic-private');
|
|
1211
|
-
expect(inspectUidProcesses(23456, performance.now() + 10_000, {}, (() => ({ status: 0,
|
|
1212
|
-
stdout: '501 300 1 S synthetic-private-name\n', stderr: '' })) as typeof spawnSync)).toMatchObject({ available: true, count: 0, processes: [] });
|
|
1213
|
-
for (const result of [
|
|
1214
|
-
{ status: 1, stdout: '', stderr: '' }, { status: 0, stdout: '', stderr: '' },
|
|
1215
|
-
{ status: 0, stdout: 'truncated-private-row', stderr: '' },
|
|
1216
|
-
{ status: null, stdout: null, stderr: null, error: new Error('synthetic-private-error') },
|
|
1217
|
-
]) expect(inspectUidProcesses(23456, performance.now() + 10_000, {}, (() => result) as unknown as typeof spawnSync)).toEqual({ available: false });
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
test('numeric UID process filtering runs through the real global process table', () => {
|
|
1221
|
-
const observation = inspectUidProcesses(23456, performance.now() + 10_000, { HOME: root, PATH: '/usr/bin:/bin' });
|
|
1222
|
-
expect(observation.available).toBe(true);
|
|
1223
|
-
expect('count' in observation).toBe(true);
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
test('the registered domain probe binds its creator check to the shell exec PID and bounds the query', () => {
|
|
1227
|
-
const env = { PATH: '/usr/bin:/bin', HOME: root };
|
|
1228
|
-
let called = false;
|
|
1229
|
-
const observation = inspectUserDomain(23456, performance.now() + 10_000, env, ((command: string, args: string[], options: any) => {
|
|
1230
|
-
called = true;
|
|
1231
|
-
expect(command).toBe('/usr/bin/sudo');
|
|
1232
|
-
expect(args).toEqual(['-n', '/bin/sh', '-c', 'printf "GSTACK_DIA_DOMAIN_PROBE_PID=%s\\n" "$$"; exec /bin/launchctl print "$1"',
|
|
1233
|
-
'gstack-dia-domain-probe', 'user/23456']);
|
|
1234
|
-
expect(options.env).toBe(env);
|
|
1235
|
-
expect(options.timeout).toBeGreaterThan(0);
|
|
1236
|
-
expect(options.timeout).toBeLessThanOrEqual(3_000);
|
|
1237
|
-
expect(Number.isInteger(options.timeout)).toBe(true);
|
|
1238
|
-
expect(options.maxBuffer).toBe(1024 * 1024);
|
|
1239
|
-
return { status: 0, stdout: 'GSTACK_DIA_DOMAIN_PROBE_PID=4567\nuser/23456 = {\n\ttype = user\n\tcreator = launchctl.4567\n}', stderr: '' };
|
|
1240
|
-
}) as typeof spawnSync);
|
|
1241
|
-
expect(called).toBe(true);
|
|
1242
|
-
expect(observation.state).toBe('present');
|
|
1243
|
-
expect(observation.structure?.creatorIsProbe).toBe(true);
|
|
1244
|
-
expect(JSON.stringify(observation)).not.toContain('GSTACK_DIA_DOMAIN_PROBE_PID');
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
test('domain probe failures preserve unknown state instead of manufacturing absence or creator ownership', () => {
|
|
1248
|
-
for (const result of [
|
|
1249
|
-
{ status: 0, stdout: 'user/23456 = {\n\ttype = user\n}', stderr: '' },
|
|
1250
|
-
{ status: 0, stdout: 'GSTACK_DIA_DOMAIN_PROBE_PID=9007199254740992\nuser/23456 = {\n\ttype = user\n}', stderr: '' },
|
|
1251
|
-
{ status: 113, stdout: '', stderr: 'Could not find domain for user uid: 23456' },
|
|
1252
|
-
{ status: null, stdout: null, stderr: null, error: new Error('synthetic-private-error') },
|
|
1253
|
-
]) {
|
|
1254
|
-
const observation = inspectUserDomain(23456, performance.now() + 10_000, {}, (() => result) as unknown as typeof spawnSync);
|
|
1255
|
-
expect(observation.state).toBe('unavailable');
|
|
1256
|
-
expect(observation.structure).toBeUndefined();
|
|
1257
|
-
expect(JSON.stringify(observation)).not.toContain('synthetic-private');
|
|
1258
|
-
}
|
|
1259
|
-
expect(inspectUserDomain(23456, performance.now() + 10_000, {}, (() => ({ status: 113,
|
|
1260
|
-
stdout: 'GSTACK_DIA_DOMAIN_PROBE_PID=4567\n', stderr: 'Could not find domain for user uid: 23456' })) as typeof spawnSync).state).toBe('absent');
|
|
1261
|
-
const never = (() => { throw new Error('must_not_spawn'); }) as typeof spawnSync;
|
|
1262
|
-
expect(() => inspectUserDomain(0, performance.now() + 10_000, {}, never)).toThrow('invalid_fresh_user_domain');
|
|
1263
|
-
for (const deadline of [0, NaN, Infinity]) expect(() => inspectUserDomain(23456, deadline, {}, never)).toThrow('fresh_launcher_deadline');
|
|
1264
|
-
});
|
|
1265
|
-
|
|
1266
|
-
test('the domain probe PID protocol survives real shell exec without logging the PID or child output', () => {
|
|
1267
|
-
const fixture = path.join(root, 'domain-probe.cjs');
|
|
1268
|
-
writeFileSync(fixture, 'process.stdout.write("user/23456 = {\\n\\ttype = user\\n\\tcreator = launchctl." + process.pid + "\\n}");');
|
|
1269
|
-
const observation = inspectUserDomain(23456, performance.now() + 10_000, { PATH: '/usr/bin:/bin', HOME: root },
|
|
1270
|
-
((_command: string, args: string[], options: any) => spawnSync('/bin/sh', ['-c',
|
|
1271
|
-
args[3].replace('/bin/launchctl print "$1"', '"$1" "$2"'), args[4], process.execPath, fixture],
|
|
1272
|
-
{ ...options, timeout: 3_000 })) as typeof spawnSync);
|
|
1273
|
-
expect(observation.state).toBe('present');
|
|
1274
|
-
expect(observation.structure?.creatorIsProbe).toBe(true);
|
|
1275
|
-
expect(observation.structure?.complete).toBe(true);
|
|
1276
|
-
expect(JSON.stringify(observation)).not.toContain('GSTACK_DIA_DOMAIN_PROBE_PID');
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
|
-
test('user-domain teardown is bound to the new account and its pre-creation absence proof', () => {
|
|
1280
|
-
const account = { guid: 'A38AC39B-5960-4F0C-B02F-C32A4F625B33', uid: 23456, gid: 23456, home: '/private/tmp/dn-fixture/home' };
|
|
1281
|
-
const record = { GeneratedUID: account.guid, UniqueID: '23456', PrimaryGroupID: '23456', NFSHomeDirectory: account.home };
|
|
1282
|
-
const proof = classifyParentDomain(23456, { status: 0, stdout: 'system = {\n\ttype = system\n\tsubdomains = {}\n}', stderr: '' });
|
|
1283
|
-
const current = classifyParentDomain(23456, { status: 0, stdout: 'system = {\n\ttype = system\n\tsubdomains = {\n\t\tuser/23456\n\t}\n}', stderr: '' });
|
|
1284
|
-
expect(ownedUserDomainTarget(record, account, proof, current, 501)).toBe('user/23456');
|
|
1285
|
-
expect(ownedUserDomainTarget(record, account, proof, proof, 501)).toBeNull();
|
|
1286
|
-
for (const key of ['GeneratedUID', 'UniqueID', 'PrimaryGroupID', 'NFSHomeDirectory']) {
|
|
1287
|
-
expect(() => ownedUserDomainTarget({ ...record, [key]: 'changed' }, account, proof, current, 501)).toThrow('fresh_user_domain_ownership_unconfirmed');
|
|
1288
|
-
}
|
|
1289
|
-
expect(() => ownedUserDomainTarget(record, account, undefined, current, 501)).toThrow('fresh_user_domain_ownership_unconfirmed');
|
|
1290
|
-
expect(() => ownedUserDomainTarget(record, account, { ...proof, state: 'present' }, current, 501)).toThrow('fresh_user_domain_ownership_unconfirmed');
|
|
1291
|
-
expect(() => ownedUserDomainTarget(record, account, { ...proof, uid: 23457 }, current, 501)).toThrow('fresh_user_domain_ownership_unconfirmed');
|
|
1292
|
-
expect(() => ownedUserDomainTarget(record, account, proof, current, 23456)).toThrow('fresh_user_domain_ownership_unconfirmed');
|
|
1293
|
-
expect(() => ownedUserDomainTarget(record, account, proof, current, NaN)).toThrow('fresh_user_domain_ownership_unconfirmed');
|
|
1294
|
-
for (const changed of [{ ...current, matchingGuiDomains: 1 }, { ...current, uid: 23457 }, { ...current, state: 'unavailable' as const },
|
|
1295
|
-
{ ...current, duplicateEntries: 1 }, { ...current, matchingUserDomains: 2 }, { ...current, parseStage: 'missing_subdomains' },
|
|
1296
|
-
{ ...current, unrecognizedEntries: 1 }]) {
|
|
1297
|
-
expect(() => ownedUserDomainTarget(record, account, proof, changed, 501)).toThrow('fresh_user_domain_ownership_unconfirmed');
|
|
1298
|
-
}
|
|
1299
|
-
});
|
|
1300
|
-
|
|
1301
|
-
test('duplicated native subdomain entries and equivalent aliases never authorize teardown or absence', () => {
|
|
1302
|
-
for (const entries of [
|
|
1303
|
-
['user/23456', 'user/23456'], ['user/23456', 'com.apple.xpc.launchd.domain.user.23456'],
|
|
1304
|
-
['user/23456', 'gui/23456', 'gui/23456'], ['user/501', 'user/501'],
|
|
1305
|
-
['user/23456', 'pid/22', 'com.apple.xpc.launchd.domain.pid.synthetic-private-process.22'],
|
|
1306
|
-
]) {
|
|
1307
|
-
const observation = classifyParentDomain(23456, { status: 0,
|
|
1308
|
-
stdout: 'system = {\n\ttype = system\n\tsubdomains = {\n' + entries.map(entry => '\t\t' + entry).join('\n') + '\n\t}\n}', stderr: '' });
|
|
1309
|
-
expect(observation.duplicateEntries).toBe(1);
|
|
1310
|
-
expect(observation.parseStage).toBe('duplicate_subdomain');
|
|
1311
|
-
expect(observation.state).toBe('unavailable');
|
|
1312
|
-
expect(passiveUserDomainState(observation, 23456)).toBe('unavailable');
|
|
1313
|
-
}
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
test('passive absence and presence require consistent complete facts with no GUI association', () => {
|
|
1317
|
-
const absent = classifyParentDomain(23456, { status: 0, stdout: 'system = {\n\ttype = system\n\tsubdomains = {}\n}', stderr: '' });
|
|
1318
|
-
expect(passiveUserDomainState(absent, 23456)).toBe('absent');
|
|
1319
|
-
for (const changed of [undefined, { ...absent, uid: 23457 }, { ...absent, matchingUserDomains: 1 },
|
|
1320
|
-
{ ...absent, matchingGuiDomains: 1 }, { ...absent, exitCode: 1 }, { ...absent, duplicateEntries: 1 },
|
|
1321
|
-
{ ...absent, unrecognizedEntries: 1 }, { ...absent, state: 'present' as const }]) {
|
|
1322
|
-
expect(passiveUserDomainState(changed, 23456)).toBe('unavailable');
|
|
1323
|
-
}
|
|
1324
|
-
const gui = classifyParentDomain(23456, { status: 0,
|
|
1325
|
-
stdout: 'system = {\n\ttype = system\n\tsubdomains = {\n\t\tuser/23456\n\t\tgui/23456\n\t}\n}', stderr: '' });
|
|
1326
|
-
expect(passiveUserDomainState(gui, 23456)).toBe('unavailable');
|
|
1327
|
-
const oldTargetObservation = classifyUserDomain(23456, { status: 113, stdout: '', stderr: 'Could not find domain for user uid: 23456' });
|
|
1328
|
-
expect(passiveUserDomainState(oldTargetObservation as any, 23456)).toBe('unavailable');
|
|
1329
|
-
});
|
|
1330
|
-
|
|
1331
|
-
test('directory UID inventory retains occupied candidate IDs and refuses malformed or empty listings', () => {
|
|
1332
|
-
expect([...parseDirectoryIds('root 0\nfixture 20000\nnobody -2\nother fixture 20001 \n')]).toEqual([0, 20000, -2, 20001]);
|
|
1333
|
-
for (const value of ['', 'malformed', 'root 0\nfixture unknown\n', 'root 0\nfixture 9007199254740992\n']) {
|
|
1334
|
-
expect(() => parseDirectoryIds(value)).toThrow();
|
|
1335
|
-
}
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1338
|
-
test('normal qualification never invokes the materializing target-domain diagnostic', () => {
|
|
1339
|
-
const implementation = runFreshAccountQualification.toString();
|
|
1340
|
-
expect(implementation).toContain('inspectParentDomain');
|
|
1341
|
-
expect(implementation).toContain('passiveUserDomainState');
|
|
1342
|
-
expect(implementation).not.toMatch(/inspectUserDomain|probeUserDomain|classifyUserDomain/);
|
|
1343
|
-
expect(implementation).not.toMatch(/['"]print['"],\s*['"](?:user|gui)\//);
|
|
1344
|
-
});
|
|
1345
|
-
|
|
1346
|
-
test('launchd receives a one-shot fresh-user security session without an Aqua or auto-login workaround', () => {
|
|
1347
|
-
const account: any = { label: 'ai.gstack.dia.fixture', account: 'gsdiafixture', bun: '/private/tmp/fixture/bin/bun',
|
|
1348
|
-
snapshot: '/private/tmp/fixture/repo', configFile: '/private/tmp/fixture/account.json', environment: { HOME: '/private/tmp/fixture/home', CI: 'true' } };
|
|
1349
|
-
const definition = freshLaunchDefinition(account);
|
|
1350
|
-
expect(definition.UserName).toBe(account.account);
|
|
1351
|
-
expect(definition.GroupName).toBe(account.account);
|
|
1352
|
-
expect(definition.SessionCreate).toBe(true);
|
|
1353
|
-
expect(definition.RunAtLoad).toBe(true);
|
|
1354
|
-
expect(definition.KeepAlive).toBe(false);
|
|
1355
|
-
expect(definition.Umask).toBe(63);
|
|
1356
|
-
expect(definition.ProgramArguments[0]).toBe(account.bun);
|
|
1357
|
-
expect(definition.ProgramArguments).toContain('--fresh-worker');
|
|
1358
|
-
expect(definition.StandardOutPath).toBe('/dev/null');
|
|
1359
|
-
expect(definition.StandardErrorPath).toBe('/dev/null');
|
|
1360
|
-
expect(JSON.stringify(definition)).not.toMatch(/Aqua|autoLogin|LoginWindow|GITHUB_TOKEN/);
|
|
1361
|
-
});
|
|
1362
|
-
|
|
1363
|
-
test('service cleanup binds the exact system label, executable, user, and private group', () => {
|
|
1364
|
-
const owner = { label: 'ai.gstack.dia.fixture', bun: '/private/tmp/fixture/bin/bun', account: 'gsdiafixture' };
|
|
1365
|
-
const state = `system/${owner.label} = {\n program = ${owner.bun}\n username = ${owner.account}\n group = ${owner.account}\n}`;
|
|
1366
|
-
expect(ownsLaunchService(state, owner)).toBe(true);
|
|
1367
|
-
for (const replacement of [state.replace('system/', 'gui/501/'), state.replace(owner.label, 'unrelated'),
|
|
1368
|
-
state.replace(owner.bun, '/unrelated/bun'), state.replace('username = gsdiafixture', 'username = runner'),
|
|
1369
|
-
state.replace('group = gsdiafixture', 'group = staff')]) expect(ownsLaunchService(replacement, owner)).toBe(false);
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
test('the fresh-account launcher refuses this non-authorized invocation without privileged work', () => {
|
|
1373
|
-
const launcher = path.resolve(import.meta.dir, '../../.github/scripts/run-dia-native-qualification.ts');
|
|
1374
|
-
const result = spawnSync(process.execPath, ['--no-env-file', '--no-install', '--no-macros', `--config=${process.platform === 'win32' ? 'NUL' : '/dev/null'}`, launcher], {
|
|
1375
|
-
cwd: root, env: { HOME: root, PATH: path.dirname(process.execPath) }, encoding: 'utf8', timeout: 10_000,
|
|
1376
|
-
});
|
|
1377
|
-
expect(result.status).toBe(2);
|
|
1378
|
-
expect(result.stderr).toBe('');
|
|
1379
|
-
expect(JSON.parse(result.stdout).reason).toBe('fresh_account_launcher_preflight_failed');
|
|
1380
|
-
});
|
|
1381
|
-
|
|
1382
|
-
for (const shape of ['normal', 'safe-link', 'traversal', 'absolute', 'escape-link', 'symlink-parent', 'symlink-dotdot', 'hardlink', 'case-collision', 'unicode-collision', 'device']) {
|
|
1383
|
-
test(`archive preflight classifies ${shape} before extraction`, () => {
|
|
1384
|
-
const python = Bun.which('python3');
|
|
1385
|
-
if (!python) throw new Error('Python 3 is required for archive boundary tests');
|
|
1386
|
-
const archive = path.join(root, shape + '.tar');
|
|
1387
|
-
const create = spawnSync(python, ['-I', '-c', `
|
|
1388
|
-
import io, sys, tarfile
|
|
1389
|
-
shape, file = sys.argv[1:]
|
|
1390
|
-
with tarfile.open(file, 'w') as out:
|
|
1391
|
-
def entry(name, kind=tarfile.REGTYPE, link=''):
|
|
1392
|
-
item = tarfile.TarInfo(name); item.type = kind; item.linkname = link
|
|
1393
|
-
if kind == tarfile.REGTYPE:
|
|
1394
|
-
item.size = 1; out.addfile(item, io.BytesIO(b'x'))
|
|
1395
|
-
else: out.addfile(item)
|
|
1396
|
-
if shape == 'normal': entry('src/file.ts')
|
|
1397
|
-
elif shape == 'safe-link': entry('target'); entry('link', tarfile.SYMTYPE, 'target')
|
|
1398
|
-
elif shape == 'traversal': entry('../outside')
|
|
1399
|
-
elif shape == 'absolute': entry('/outside')
|
|
1400
|
-
elif shape == 'escape-link': entry('link', tarfile.SYMTYPE, '../outside')
|
|
1401
|
-
elif shape == 'symlink-parent': entry('link', tarfile.SYMTYPE, 'target'); entry('link/child')
|
|
1402
|
-
elif shape == 'symlink-dotdot': entry('b', tarfile.SYMTYPE, '.'); entry('a/link', tarfile.SYMTYPE, '../b/..')
|
|
1403
|
-
elif shape == 'hardlink': entry('target'); entry('link', tarfile.LNKTYPE, 'target')
|
|
1404
|
-
elif shape == 'case-collision': entry('File'); entry('file')
|
|
1405
|
-
elif shape == 'unicode-collision': entry('Caf' + chr(233)); entry('Cafe' + chr(769))
|
|
1406
|
-
elif shape == 'device': entry('device', tarfile.CHRTYPE)
|
|
1407
|
-
`, shape, archive], { encoding: 'utf8', timeout: 10_000 });
|
|
1408
|
-
expect(create.status).toBe(0);
|
|
1409
|
-
const checked = spawnSync(python, ['-I', '-c', ARCHIVE_CHECK, archive], { encoding: 'utf8', timeout: 10_000 });
|
|
1410
|
-
expect(checked.status).toBe(['normal', 'safe-link'].includes(shape) ? 0 : 2);
|
|
1411
|
-
expect(JSON.parse(checked.stdout).valid).toBe(['normal', 'safe-link'].includes(shape));
|
|
1412
|
-
expect(checked.stderr).toBe('');
|
|
1413
|
-
});
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
test('receipt collection rejects symlinks and an unrelated owner without printing content', () => {
|
|
1417
|
-
const python = Bun.which('python3');
|
|
1418
|
-
if (!python) throw new Error('Python 3 is required for receipt boundary tests');
|
|
1419
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'receipts-')));
|
|
1420
|
-
const file = path.join(directory, 'receipt.json');
|
|
1421
|
-
writeFileSync(file, JSON.stringify({ status: 'incomplete', reason: 'synthetic_fixture' }), { mode: 0o600 });
|
|
1422
|
-
chmodSync(file, 0o600);
|
|
1423
|
-
const uid = process.getuid!();
|
|
1424
|
-
const read = (selected: string, owner: number) => spawnSync(python, ['-I', '-c', PRIVATE_RECEIPT_READ, selected, String(owner), directory], {
|
|
1425
|
-
encoding: 'utf8', timeout: 10_000,
|
|
1426
|
-
});
|
|
1427
|
-
const valid = read(file, uid);
|
|
1428
|
-
expect(valid.status).toBe(0);
|
|
1429
|
-
expect(JSON.parse(valid.stdout).status).toBe('incomplete');
|
|
1430
|
-
const wrong = read(file, uid + 1);
|
|
1431
|
-
expect(wrong.status).toBe(2);
|
|
1432
|
-
expect(wrong.stdout).toBe('');
|
|
1433
|
-
const link = path.join(directory, 'linked.json');
|
|
1434
|
-
symlinkSync(file, link);
|
|
1435
|
-
const linked = read(link, uid);
|
|
1436
|
-
expect(linked.status).toBe(2);
|
|
1437
|
-
expect(linked.stdout).toBe('');
|
|
1438
|
-
});
|
|
1439
|
-
|
|
1440
|
-
test('receipt collection rejects linked ancestors and incomplete JSON without disclosing content', () => {
|
|
1441
|
-
const python = Bun.which('python3');
|
|
1442
|
-
if (!python) throw new Error('Python 3 is required for receipt boundary tests');
|
|
1443
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'receipt-ancestors-')));
|
|
1444
|
-
const nested = path.join(directory, 'nested');
|
|
1445
|
-
mkdirSync(nested, { mode: 0o700 });
|
|
1446
|
-
const file = path.join(nested, 'receipt.json');
|
|
1447
|
-
writePrivateReceipt(file, { status: 'incomplete' });
|
|
1448
|
-
const linked = path.join(directory, 'linked');
|
|
1449
|
-
symlinkSync(nested, linked, 'dir');
|
|
1450
|
-
const read = (selected: string) => spawnSync(python, ['-I', '-c', PRIVATE_RECEIPT_READ, selected, String(process.getuid!()), directory], {
|
|
1451
|
-
encoding: 'utf8', timeout: 3_000,
|
|
1452
|
-
});
|
|
1453
|
-
expect(read(file).status).toBe(0);
|
|
1454
|
-
const rejected = read(path.join(linked, 'receipt.json'));
|
|
1455
|
-
expect(rejected.status).toBe(2);
|
|
1456
|
-
expect(rejected.stdout).toBe('');
|
|
1457
|
-
writeFileSync(file, '{"synthetic-private-value":');
|
|
1458
|
-
const partial = read(file);
|
|
1459
|
-
expect(partial.status).toBe(2);
|
|
1460
|
-
expect(partial.stdout).toBe('');
|
|
1461
|
-
});
|
|
1462
|
-
|
|
1463
|
-
test('receipt publication is private, atomic, and refuses unintended overwrites or symlink targets', () => {
|
|
1464
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'atomic-receipt-')));
|
|
1465
|
-
const file = path.join(directory, 'receipt.json');
|
|
1466
|
-
writePrivateReceipt(file, { status: 'incomplete', phase: 'first' });
|
|
1467
|
-
expect(lstatSync(file).mode & 0o777).toBe(0o600);
|
|
1468
|
-
expect(() => writePrivateReceipt(file, { status: 'passed' })).toThrow();
|
|
1469
|
-
expect(JSON.parse(readFileSync(file, 'utf8')).phase).toBe('first');
|
|
1470
|
-
writePrivateReceipt(file, { status: 'incomplete', phase: 'second' }, true);
|
|
1471
|
-
expect(JSON.parse(readFileSync(file, 'utf8')).phase).toBe('second');
|
|
1472
|
-
const link = path.join(directory, 'linked.json');
|
|
1473
|
-
symlinkSync(file, link);
|
|
1474
|
-
expect(() => writePrivateReceipt(link, { status: 'passed' }, true)).toThrow('unsafe_receipt_replacement');
|
|
1475
|
-
expect(JSON.parse(readFileSync(file, 'utf8')).phase).toBe('second');
|
|
1476
|
-
expect(readdirSync(directory).some(name => name.startsWith('.dia-receipt-'))).toBe(false);
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
test('an atomic owner-checked diagnostic receipt is readable while its producer is still alive', async () => {
|
|
1480
|
-
const python = Bun.which('python3');
|
|
1481
|
-
if (!python) throw new Error('Python 3 is required for receipt boundary tests');
|
|
1482
|
-
const directory = realpathSync(mkdtempSync(path.join(root, 'live-receipt-')));
|
|
1483
|
-
const file = path.join(directory, 'receipt.json');
|
|
1484
|
-
const module = pathToFileURL(path.resolve(import.meta.dir, '../../.github/scripts/qualify-dia-macos.ts')).href;
|
|
1485
|
-
const child = Bun.spawn([process.execPath, '--no-env-file', '--no-install', '--no-macros', '--config=/dev/null', '-e', `
|
|
1486
|
-
const { writePrivateReceipt } = await import(${JSON.stringify(module)});
|
|
1487
|
-
writePrivateReceipt(${JSON.stringify(file)}, { status: 'incomplete', reason: 'worker_diagnostic', cleanup: { complete: false } });
|
|
1488
|
-
console.log('ready');
|
|
1489
|
-
setInterval(() => {}, 1000);
|
|
1490
|
-
`], { cwd: directory, env: { HOME: directory, PATH: path.dirname(process.execPath) }, stdout: 'pipe', stderr: 'pipe' });
|
|
1491
|
-
try {
|
|
1492
|
-
const reader = child.stdout.getReader();
|
|
1493
|
-
expect(new TextDecoder().decode((await reader.read()).value)).toContain('ready');
|
|
1494
|
-
reader.releaseLock();
|
|
1495
|
-
expect(child.exitCode).toBeNull();
|
|
1496
|
-
const result = spawnSync(python, ['-I', '-c', PRIVATE_RECEIPT_READ, file, String(process.getuid!()), directory], {
|
|
1497
|
-
encoding: 'utf8', timeout: 3_000,
|
|
1498
|
-
});
|
|
1499
|
-
expect(result.status).toBe(0);
|
|
1500
|
-
expect(JSON.parse(result.stdout)).toEqual({ status: 'incomplete', reason: 'worker_diagnostic', cleanup: { complete: false } });
|
|
1501
|
-
expect(child.exitCode).toBeNull();
|
|
1502
|
-
} finally {
|
|
1503
|
-
child.kill();
|
|
1504
|
-
await child.exited;
|
|
1505
|
-
}
|
|
1506
|
-
});
|
|
1507
|
-
|
|
1508
|
-
test('UID process diagnostics preserve zombie/service/browser distinctions without argv or arbitrary names', () => {
|
|
1509
|
-
const facts = uidProcessFacts('20000 301 1 Ss cfprefsd\n20000 302 1 Z bun\n20000 303 301 R Google Chrome for Testing\n20000 304 1 S synthetic-private-value\n501 999 1 S security\n', 20000);
|
|
1510
|
-
expect(facts).toEqual({ available: true, count: 4, zombies: 1, live: 3, truncated: false, processes: [
|
|
1511
|
-
{ pid: 301, ppid: 1, state: 'S', basename: 'cfprefsd' }, { pid: 302, ppid: 1, state: 'Z', basename: 'bun' },
|
|
1512
|
-
{ pid: 303, ppid: 301, state: 'R', basename: 'Google Chrome for Testing' }, { pid: 304, ppid: 1, state: 'S', basename: 'other' },
|
|
1513
|
-
] });
|
|
1514
|
-
expect(JSON.stringify(facts)).not.toContain('synthetic-private-value');
|
|
1515
|
-
expect(JSON.stringify(facts)).not.toContain('999');
|
|
1516
|
-
expect(uidProcessFacts('20000 401 1 S cfprefsd\n', 20000).processes[0].pid).toBe(401);
|
|
1517
|
-
expect(uidProcessFacts('20000 402 1 Z+ bun\n', 20000)).toMatchObject({ count: 1, zombies: 1, live: 0 });
|
|
1518
|
-
expect(() => uidProcessFacts('malformed synthetic-private-value', 20000)).toThrow('invalid_uid_process_snapshot');
|
|
1519
|
-
expect(uidProcessFacts(Array.from({ length: 80 }, (_, index) => `20000 ${1000 + index} 1 S bun`).join('\n'), 20000))
|
|
1520
|
-
.toMatchObject({ count: 80, truncated: true });
|
|
1521
|
-
expect(uidProcessFacts(Array.from({ length: 80 }, (_, index) => `20000 ${1000 + index} 1 S bun`).join('\n'), 20000).processes).toHaveLength(64);
|
|
1522
|
-
});
|
|
1523
|
-
|
|
1524
|
-
test('captured passing inner receipts cannot qualify a run with incomplete cleanup', () => {
|
|
1525
|
-
const cleanup = { serviceStopped: true, userDomainStopped: true, userProcessesStopped: true, accountRemoved: true, groupRemoved: true, stagingRemoved: true };
|
|
1526
|
-
expect(freshQualificationPassed(0, 'passed', 'passed', cleanup)).toBe(true);
|
|
1527
|
-
for (const key of Object.keys(cleanup)) expect(freshQualificationPassed(0, 'passed', 'passed', { ...cleanup, [key]: false })).toBe(false);
|
|
1528
|
-
expect(freshQualificationPassed(0, 'passed', 'passed', {})).toBe(false);
|
|
1529
|
-
expect(freshQualificationPassed(2, 'passed', 'passed', cleanup)).toBe(false);
|
|
1530
|
-
expect(freshQualificationPassed(0, 'incomplete', 'passed', cleanup)).toBe(false);
|
|
1531
|
-
});
|
|
1532
|
-
|
|
1533
|
-
test('the registered spawn observer records the actual owned child and closes launch admission', async () => {
|
|
1534
|
-
const profile = path.join(root, 'profile');
|
|
1535
|
-
const childProcess = require('node:child_process');
|
|
1536
|
-
const original = childProcess.spawn;
|
|
1537
|
-
const observer = observeBrowserLaunches(new Map([[process.execPath, profile]]));
|
|
1538
|
-
const args = ['--no-env-file', '--no-install', '-e', 'process.exit(0)', '--', '--remote-debugging-pipe', '--user-data-dir=' + profile];
|
|
1539
|
-
const options = { cwd: root, detached: true, shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
1540
|
-
env: { PATH: path.dirname(process.execPath), HOME: root, ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}) } };
|
|
1541
|
-
try {
|
|
1542
|
-
const child = childProcess.spawn(process.execPath, args, options);
|
|
1543
|
-
const code = await new Promise(resolve => child.once('close', resolve));
|
|
1544
|
-
expect(code).toBe(0);
|
|
1545
|
-
expect(observer.children).toHaveLength(1);
|
|
1546
|
-
expect(observer.children[0].process).toBe(child);
|
|
1547
|
-
expect(observer.children[0].pid).toBe(child.pid);
|
|
1548
|
-
observer.stop();
|
|
1549
|
-
expect(() => childProcess.spawn(process.execPath, args, options)).toThrow('browser_launch_policy_rejected');
|
|
1550
|
-
expect(observer.children).toHaveLength(1);
|
|
1551
|
-
} finally {
|
|
1552
|
-
observer.restore();
|
|
1553
|
-
}
|
|
1554
|
-
expect(childProcess.spawn).toBe(original);
|
|
1555
|
-
});
|
|
1556
|
-
|
|
1557
|
-
test('the owned spawn stderr observer preserves the consumer stream and removes only its own listeners', async () => {
|
|
1558
|
-
const childProcess = require('node:child_process');
|
|
1559
|
-
const original = childProcess.spawn;
|
|
1560
|
-
const profile = path.join(root, 'stderr-observer-profile');
|
|
1561
|
-
const observer = observeBrowserLaunches(new Map([[process.execPath, profile]]));
|
|
1562
|
-
const text = 'DevTools remote debugging requires a non-default data directory.\nsynthetic-private-value\n';
|
|
1563
|
-
try {
|
|
1564
|
-
const child = childProcess.spawn(process.execPath, ['--no-env-file', '--no-install', '-e',
|
|
1565
|
-
`process.stderr.write(${JSON.stringify(text)})`, '--', '--remote-debugging-pipe', '--user-data-dir=' + profile], {
|
|
1566
|
-
detached: true, shell: false, stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'], env: { HOME: root, PATH: path.dirname(process.execPath) },
|
|
1567
|
-
});
|
|
1568
|
-
const chunks: Buffer[] = [];
|
|
1569
|
-
const consumer = (chunk: Buffer) => { chunks.push(chunk); };
|
|
1570
|
-
child.stderr.on('data', consumer);
|
|
1571
|
-
const code = await new Promise(resolve => child.once('close', resolve));
|
|
1572
|
-
expect(code).toBe(0);
|
|
1573
|
-
expect(Buffer.concat(chunks).toString()).toBe(text);
|
|
1574
|
-
const facts = browserStderrFacts(observer.children);
|
|
1575
|
-
expect(facts).toHaveLength(1);
|
|
1576
|
-
expect(facts[0]).toMatchObject({ available: true, bytesSeen: Buffer.byteLength(text), ended: true,
|
|
1577
|
-
reasonCounts: { default_profile_policy: 1 } });
|
|
1578
|
-
expect(JSON.stringify(facts)).not.toContain('synthetic-private');
|
|
1579
|
-
observer.restore();
|
|
1580
|
-
expect(child.stderr.listeners('data')).toContain(consumer);
|
|
1581
|
-
expect(childProcess.spawn).toBe(original);
|
|
1582
|
-
} finally { observer.restore(); }
|
|
1583
|
-
});
|
|
1584
|
-
|
|
1585
|
-
test('an owned killed child is joined before proving its process group absent', async () => {
|
|
1586
|
-
const childProcess = require('node:child_process');
|
|
1587
|
-
const profile = path.join(root, 'reaping-profile');
|
|
1588
|
-
const observer = observeBrowserLaunches(new Map([[process.execPath, profile]]));
|
|
1589
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1590
|
-
try {
|
|
1591
|
-
const child = childProcess.spawn(process.execPath, ['--no-env-file', '--no-install', '-e',
|
|
1592
|
-
'process.stdout.write("ready\\n"); setInterval(() => {}, 1000)', '--', '--remote-debugging-pipe', '--user-data-dir=' + profile], {
|
|
1593
|
-
detached: true, shell: false, stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'], env: { HOME: root, PATH: path.dirname(process.execPath) },
|
|
1594
|
-
});
|
|
1595
|
-
await Promise.race([new Promise(resolve => child.stdout.once('data', resolve)), new Promise((_, reject) => {
|
|
1596
|
-
timer = setTimeout(() => reject(new Error('fixture_startup_timeout')), 3000);
|
|
1597
|
-
})]);
|
|
1598
|
-
clearTimeout(timer);
|
|
1599
|
-
const owned = observer.children[0];
|
|
1600
|
-
expect(owned.closeObserved).toBe(false);
|
|
1601
|
-
const until = performance.now() + 5_000;
|
|
1602
|
-
const facts: Record<string, any> = { signalSent: false, absenceConfirmed: false };
|
|
1603
|
-
await stopOwnedBrowserGroup(owned, until, facts);
|
|
1604
|
-
expect(owned.closeObserved).toBe(true);
|
|
1605
|
-
expect(facts).toMatchObject({ signalSent: true, childCloseObserved: true, absenceConfirmed: true });
|
|
1606
|
-
let absent = false;
|
|
1607
|
-
while (performance.now() < until) {
|
|
1608
|
-
try { process.kill(-owned.pid, 0); }
|
|
1609
|
-
catch (error: any) { if (error.code === 'ESRCH') { absent = true; break; } throw error; }
|
|
1610
|
-
await Bun.sleep(10);
|
|
1611
|
-
}
|
|
1612
|
-
expect(absent).toBe(true);
|
|
1613
|
-
expect(browserRootFacts(observer.children)[0]).toMatchObject({ closeObserved: true, signal: 'SIGKILL' });
|
|
1614
|
-
} finally {
|
|
1615
|
-
clearTimeout(timer);
|
|
1616
|
-
for (const owned of observer.children) {
|
|
1617
|
-
if (owned.closeObserved) continue;
|
|
1618
|
-
try { process.kill(-owned.pid, 'SIGKILL'); } catch {}
|
|
1619
|
-
await joinOwnedBrowserClose(owned, performance.now() + 5_000);
|
|
1620
|
-
}
|
|
1621
|
-
observer.restore();
|
|
1622
|
-
}
|
|
1623
|
-
});
|
|
1624
|
-
|
|
1625
|
-
test('the pinned Playwright launch is captured with the observer installed after importing Playwright', async () => {
|
|
1626
|
-
const { chromium } = await import('playwright');
|
|
1627
|
-
expect(require('playwright/package.json').version).toBe('1.62.1');
|
|
1628
|
-
const executable = realpathSync(chromium.executablePath());
|
|
1629
|
-
const profile = path.join(root, 'playwright-profile');
|
|
1630
|
-
const observer = observeBrowserLaunches(new Map([[executable, profile]]));
|
|
1631
|
-
let context: Awaited<ReturnType<typeof chromium.launchPersistentContext>> | undefined;
|
|
1632
|
-
try {
|
|
1633
|
-
const nativeOptions = nativeDiaLaunchOptions(executable, {
|
|
1634
|
-
HOME: root, PATH: path.dirname(process.execPath) + ':/usr/bin:/bin:/usr/sbin:/sbin',
|
|
1635
|
-
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
|
|
1636
|
-
});
|
|
1637
|
-
expect(nativeOptions.chromiumSandbox).toBe(true);
|
|
1638
|
-
context = await chromium.launchPersistentContext(profile, nativeOptions);
|
|
1639
|
-
expect(observer.children).toHaveLength(1);
|
|
1640
|
-
expect(observer.children[0].executable).toBe(executable);
|
|
1641
|
-
expect(observer.children[0].pid).toBeGreaterThan(1);
|
|
1642
|
-
expect(browserRootFacts(observer.children)).toEqual([{ pid: observer.children[0].pid, exitCode: null, closeObserved: false, signal: null }]);
|
|
1643
|
-
expect(browserStartupFacts(context.pages().map(page => page.url()), 'http://127.0.0.1:8123').allowed).toBe(true);
|
|
1644
|
-
expect(browserStderrFacts(observer.children)[0].available).toBe(true);
|
|
1645
|
-
expect(browserStderrFacts(observer.children)[0].bytesInspected).toBeLessThanOrEqual(65_536);
|
|
1646
|
-
expect(observer.attempts).toHaveLength(1);
|
|
1647
|
-
expect(observer.attempts[0]).toEqual({ admissionOpen: true, argumentsArray: true, pipeFlag: true, profileArgumentCount: 1,
|
|
1648
|
-
expectedProfile: true, detached: true, shellDisabled: true, stdioCount: 5, extraPipeDescriptors: true,
|
|
1649
|
-
headlessFlag: true, blankStartupArgument: true, tcpDebuggingFlag: false, mockKeychainFlag: false,
|
|
1650
|
-
passwordStoreFlag: false, firstRunSuppressed: false, sandboxRequired: process.platform === 'darwin', sandboxDisablingFlag: false });
|
|
1651
|
-
expect(JSON.stringify(observer.attempts)).not.toContain(executable);
|
|
1652
|
-
expect(JSON.stringify(observer.attempts)).not.toContain(profile);
|
|
1653
|
-
const page = context.pages()[0] ?? await context.newPage();
|
|
1654
|
-
await page.setContent('<div id="fixture">isolated browser smoke</div>');
|
|
1655
|
-
expect(await page.locator('#fixture').innerText()).toBe('isolated browser smoke');
|
|
1656
|
-
await context.close();
|
|
1657
|
-
context = undefined;
|
|
1658
|
-
expect(observer.children[0].process.exitCode !== null || observer.children[0].process.signalCode !== null).toBe(true);
|
|
1659
|
-
} finally {
|
|
1660
|
-
observer.stop();
|
|
1661
|
-
await context?.close();
|
|
1662
|
-
observer.restore();
|
|
1663
|
-
}
|
|
1664
|
-
}, 40_000);
|
|
1665
|
-
|
|
1666
|
-
for (const forbidden of ['--remote-debugging-port=9222', '--use-mock-keychain', '--password-store=basic', '--no-first-run']) {
|
|
1667
|
-
test(`the actual spawn callback rejects ${forbidden} before process creation`, () => {
|
|
1668
|
-
const profile = path.join(root, 'profile');
|
|
1669
|
-
const childProcess = require('node:child_process');
|
|
1670
|
-
const observer = observeBrowserLaunches(new Map([[process.execPath, profile]]));
|
|
1671
|
-
try {
|
|
1672
|
-
expect(() => childProcess.spawn(process.execPath, ['--remote-debugging-pipe', '--user-data-dir=' + profile, forbidden],
|
|
1673
|
-
{ detached: true, shell: false, stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] })).toThrow('browser_launch_policy_rejected');
|
|
1674
|
-
expect(observer.children).toHaveLength(0);
|
|
1675
|
-
} finally { observer.restore(); }
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
test('the spawn callback rejects an unowned profile, shell expansion, and a shared process group', () => {
|
|
1680
|
-
const profile = path.join(root, 'profile');
|
|
1681
|
-
const childProcess = require('node:child_process');
|
|
1682
|
-
const observer = observeBrowserLaunches(new Map([[process.execPath, profile]]));
|
|
1683
|
-
try {
|
|
1684
|
-
for (const [selected, detached, shell] of [[path.join(root, 'other'), true, false], [profile, false, false], [profile, true, true]]) {
|
|
1685
|
-
expect(() => childProcess.spawn(process.execPath, ['--remote-debugging-pipe', '--user-data-dir=' + selected], { detached, shell, stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] }))
|
|
1686
|
-
.toThrow('browser_launch_policy_rejected');
|
|
1687
|
-
}
|
|
1688
|
-
expect(() => childProcess.spawn(process.execPath, ['--remote-debugging-pipe', '--user-data-dir=' + profile, '--user-data-dir=' + path.join(root, 'other')],
|
|
1689
|
-
{ detached: true, shell: false, stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] })).toThrow('browser_launch_policy_rejected');
|
|
1690
|
-
expect(() => childProcess.spawn(process.execPath, ['--remote-debugging-pipe', '--user-data-dir=' + profile],
|
|
1691
|
-
{ detached: true, shell: false, stdio: 'inherit' })).toThrow('browser_launch_policy_rejected');
|
|
1692
|
-
expect(observer.children).toHaveLength(0);
|
|
1693
|
-
} finally { observer.restore(); }
|
|
1694
|
-
});
|
|
1695
|
-
});
|