@runcofactor/cli 0.54.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/{account-6YJAU53I.js → account-RHHS7HT4.js} +5 -5
- package/dist/{alert-Z7V2ZUEW.js → alert-B5WEOKHP.js} +8 -8
- package/dist/{api-catalog-UEDTRM6E.js → api-catalog-UXONX3BR.js} +5 -5
- package/dist/{api-key-V5GEPRRG.js → api-key-J3A4TJKN.js} +5 -5
- package/dist/{attachment-M7VZ6GSE.js → attachment-ZQW7DR4G.js} +5 -5
- package/dist/{auth-DVTMWJKF.js → auth-HQGKUZBS.js} +3 -3
- package/dist/{benchmark-KZJZYUTQ.js → benchmark-TIKJA32J.js} +5 -5
- package/dist/{blackcat-QJXYDE66.js → blackcat-Z5LNV2GS.js} +3 -3
- package/dist/{chunk-7MRZWPVC.js → chunk-3KBYI6RR.js} +3 -3
- package/dist/{chunk-LPQTXBJF.js → chunk-3TZABC7D.js} +7 -7
- package/dist/{chunk-ZOPVBINJ.js → chunk-5OKCT6LF.js} +5 -5
- package/dist/{chunk-ZO5MSM4Q.js → chunk-635IQV6B.js} +2 -2
- package/dist/{chunk-4EWSONHO.js → chunk-6X6ZTDXP.js} +4 -4
- package/dist/{chunk-ZFN5FWN2.js → chunk-BJHAVM7Z.js} +2 -2
- package/dist/{chunk-Q7UAEFW5.js → chunk-CQSQNDMV.js} +4 -4
- package/dist/{chunk-AQOHNAEN.js → chunk-CZDQVQ57.js} +5 -5
- package/dist/{chunk-CV22445U.js → chunk-EGWVL5XA.js} +2 -2
- package/dist/{chunk-FLDIN6VX.js → chunk-JQG3V5E3.js} +2 -2
- package/dist/{chunk-KNWKOKM7.js → chunk-KL2ARGH6.js} +20 -11
- package/dist/chunk-KL2ARGH6.js.map +1 -0
- package/dist/{chunk-T6EQFICG.js → chunk-L4JQ236V.js} +4 -4
- package/dist/{chunk-2M4PZK6Q.js → chunk-MB4FS35J.js} +8 -8
- package/dist/{chunk-3YNR6U6Q.js → chunk-OU4I3MEH.js} +3 -3
- package/dist/{chunk-O7MBDOMO.js → chunk-QHRWXCON.js} +25 -9
- package/dist/chunk-QHRWXCON.js.map +1 -0
- package/dist/{chunk-HGEJHS45.js → chunk-QVMGXMP5.js} +5 -5
- package/dist/{chunk-G2SCV2CY.js → chunk-R2CRH6JQ.js} +3 -3
- package/dist/{chunk-HESV3F6U.js → chunk-RV7FZ4ID.js} +3 -3
- package/dist/{chunk-7VBX2M4F.js → chunk-SCQVBLNR.js} +3 -3
- package/dist/{chunk-74TU6PGO.js → chunk-TGI2AAOZ.js} +4 -4
- package/dist/{chunk-R2CVV7QT.js → chunk-U3MRXQZS.js} +5 -1
- package/dist/chunk-U3MRXQZS.js.map +1 -0
- package/dist/{chunk-L4Y5PZUE.js → chunk-W6DL4IP6.js} +2 -2
- package/dist/{chunk-TRC63RSK.js → chunk-WWUEKQLR.js} +3 -3
- package/dist/{chunk-MQQCUU5S.js → chunk-XM3WTJ7Y.js} +2 -2
- package/dist/{chunk-YACDH6MJ.js → chunk-XWJJYZQC.js} +2 -2
- package/dist/{chunk-QGP67JOK.js → chunk-YEH7UIUY.js} +4 -4
- package/dist/{chunk-RA2NRAWU.js → chunk-YTXZUWMN.js} +6 -6
- package/dist/{chunk-JAIMLQOU.js → chunk-Z72GWGH2.js} +6 -6
- package/dist/{chunk-QYHJUHL7.js → chunk-ZH7HWVEE.js} +2 -2
- package/dist/{chunk-IVWWYKAM.js → chunk-ZZZ33KTO.js} +3 -3
- package/dist/{credential-546ER7WQ.js → credential-2IEZDTH3.js} +5 -5
- package/dist/{credential-template-BWMEBGBN.js → credential-template-S5WSOI3P.js} +5 -5
- package/dist/{digest-NIVBDDI2.js → digest-HQGL7H6E.js} +8 -8
- package/dist/{docs-AK4MDP6K.js → docs-CJQZVERZ.js} +5 -5
- package/dist/{doctor-NCLA47EL.js → doctor-EGK5AB77.js} +3 -3
- package/dist/{environment-X3PV5IN2.js → environment-MWVXYEDU.js} +7 -7
- package/dist/{eval-5FZKYZ6O.js → eval-F4NORBQI.js} +9 -9
- package/dist/{exploration-2RVGIUKT.js → exploration-JC7R7KTJ.js} +5 -5
- package/dist/{exploratory-test-SJ5HFI3L.js → exploratory-test-BHK5FTMJ.js} +13 -5
- package/dist/exploratory-test-BHK5FTMJ.js.map +1 -0
- package/dist/{external-run-4NDVAHQB.js → external-run-UL5CKG54.js} +9 -9
- package/dist/{feature-flag-C4IRE724.js → feature-flag-NMO25TZN.js} +5 -5
- package/dist/{feedback-337OZCKC.js → feedback-3JNPSUXS.js} +6 -6
- package/dist/{flow-D7ZLBKCH.js → flow-AYE4RTZN.js} +5 -5
- package/dist/{github-LKXYIALH.js → github-DM2DCXRY.js} +6 -6
- package/dist/index.js +76 -76
- package/dist/{init-GKEC6LPH.js → init-CPDT5XAU.js} +5 -5
- package/dist/{install-K45SAT2V.js → install-JZRNCHMO.js} +5 -5
- package/dist/{issues-2HMTQYWR.js → issues-GRFJB52C.js} +8 -8
- package/dist/{knobs-KUCGLO5Z.js → knobs-3Y4QXWAD.js} +5 -5
- package/dist/{landing-KAET2I73.js → landing-ABYL5WVG.js} +7 -7
- package/dist/{list-GDV4UYWV.js → list-WG5QB4NK.js} +5 -5
- package/dist/{local-NFOSJLLV.js → local-KX24OR5Z.js} +7 -7
- package/dist/{login-Y77HWROY.js → login-MLJQAIFK.js} +5 -5
- package/dist/{mcp-CPCFMU6B.js → mcp-DDTO54XD.js} +10 -10
- package/dist/{metrics-Y245ST63.js → metrics-BJLLOSPQ.js} +5 -5
- package/dist/{monitor-BUDWSC3S.js → monitor-E2CKGHPM.js} +5 -5
- package/dist/{perf-3JOAXPHP.js → perf-IX7JOMUE.js} +11 -11
- package/dist/{product-CXL3YTMD.js → product-2ECWM2JN.js} +5 -5
- package/dist/{product-map-656BTU74.js → product-map-O542LZQL.js} +7 -7
- package/dist/{prompts-3SZ7O5E6.js → prompts-Y7O4XJ5P.js} +5 -5
- package/dist/{proofs-4CSCCSNL.js → proofs-TNOBLSRP.js} +9 -9
- package/dist/{property-IEKBXRVY.js → property-TZCGOZQG.js} +6 -6
- package/dist/{psql-M4CM6TSI.js → psql-M66YSUXW.js} +5 -5
- package/dist/{record-QP4TYZBS.js → record-3JINBRAR.js} +6 -6
- package/dist/{redis-KG47IGJV.js → redis-FOIBINYA.js} +5 -5
- package/dist/{release-QHPSQULK.js → release-B4VWMVEI.js} +5 -5
- package/dist/{runbook-YVBG2QK3.js → runbook-EYMSE37E.js} +5 -5
- package/dist/{sandbox-ZXDZTCCN.js → sandbox-R6EMGOKR.js} +16 -13
- package/dist/sandbox-R6EMGOKR.js.map +1 -0
- package/dist/{scenarios-2SX2NUCD.js → scenarios-W7YNLBVO.js} +5 -5
- package/dist/{schedule-P6F7MLXX.js → schedule-EDHQJUUZ.js} +8 -8
- package/dist/{session-SZMCZTPE.js → session-4N62APNQ.js} +9 -9
- package/dist/{skill-D4XFW6QI.js → skill-XFGMNOVM.js} +107 -9
- package/dist/skill-XFGMNOVM.js.map +1 -0
- package/dist/skills/cofactor-debug-run/SKILL.md +5 -1
- package/dist/skills/cofactor-debug-run/history/2.0.9.md +319 -0
- package/dist/skills/cofactor-debug-run/version.json +1 -1
- package/dist/{slack-UDXTPOSW.js → slack-RUAXG6TB.js} +6 -6
- package/dist/{spec-YCBBIV27.js → spec-ZZKMW7ER.js} +5 -5
- package/dist/{src-ZKVNVLXY.js → src-CQ2YAW7F.js} +3 -3
- package/dist/{src-NG7DRE5A.js → src-DGIVA22K.js} +3 -3
- package/dist/{src-DEGBASCD.js → src-ECZWYNSV.js} +5 -5
- package/dist/{src-DY7GLRO2.js → src-OKSAEHQS.js} +4 -4
- package/dist/{start-ZYZSORPY.js → start-OCZGILRL.js} +5 -5
- package/dist/{status-ESIG6A7W.js → status-YTJ6YITG.js} +5 -5
- package/dist/{tag-3K5Y25AR.js → tag-KBG4SWRL.js} +6 -6
- package/dist/{task-JLNX7O3M.js → task-O6XRMHYE.js} +6 -6
- package/dist/{teams-F4IU3EXE.js → teams-NFUHS7XI.js} +7 -7
- package/dist/{test-suite-QHDUW5W5.js → test-suite-6BH3DESN.js} +12 -12
- package/dist/{tickets-NBL5WACL.js → tickets-JU2STK4B.js} +5 -5
- package/dist/{trace-ZKYAI5JF.js → trace-NMSGKASJ.js} +8 -8
- package/dist/{triage-5NWCREUM.js → triage-JGECJJU5.js} +6 -6
- package/dist/{trigger-KXYYY5IR.js → trigger-OSPEVCCQ.js} +9 -9
- package/dist/{update-check-Q2URVJLC.js → update-check-FNP6RFZO.js} +4 -4
- package/dist/{upgrade-4XSASC7U.js → upgrade-3PPBXFAT.js} +5 -5
- package/dist/{usage-UY7VZYVY.js → usage-QMCU2JIO.js} +7 -7
- package/dist/{utilities-53NXNLA7.js → utilities-7VHWTSJT.js} +6 -6
- package/dist/{visual-IJAT7X7D.js → visual-WIZSPTVK.js} +5 -5
- package/dist/{workflow-OQNH2FIS.js → workflow-23HKTJLS.js} +62 -33
- package/dist/workflow-23HKTJLS.js.map +1 -0
- package/dist/{workflow-cache-FP6MKLVR.js → workflow-cache-ACGM3BOI.js} +16 -16
- package/dist/{workflow-cache-convergence-resolve-HS5KO7US.js → workflow-cache-convergence-resolve-KEQDQMKL.js} +5 -5
- package/dist/{workflow-debug-EM55H2QE.js → workflow-debug-TVLBNRCB.js} +11 -11
- package/dist/{workflow-feedback-GNWNWVHH.js → workflow-feedback-GB5VPGE2.js} +5 -5
- package/dist/{workflow-fork-M5AQWST7.js → workflow-fork-2IPJNR76.js} +5 -5
- package/dist/{workflow-promotion-HYULZDYL.js → workflow-promotion-QEQUXVUT.js} +5 -5
- package/dist/{workflow-proposals-EN327FSE.js → workflow-proposals-KHGTPMD4.js} +5 -5
- package/dist/{workflow-revisions-ZMTVNWRW.js → workflow-revisions-F5KQEEXB.js} +5 -5
- package/dist/{workflow-status-FNAJG34W.js → workflow-status-66HC5MEA.js} +5 -5
- package/package.json +1 -1
- package/dist/chunk-KNWKOKM7.js.map +0 -1
- package/dist/chunk-O7MBDOMO.js.map +0 -1
- package/dist/chunk-R2CVV7QT.js.map +0 -1
- package/dist/exploratory-test-SJ5HFI3L.js.map +0 -1
- package/dist/sandbox-ZXDZTCCN.js.map +0 -1
- package/dist/skill-D4XFW6QI.js.map +0 -1
- package/dist/workflow-OQNH2FIS.js.map +0 -1
- /package/dist/{account-6YJAU53I.js.map → account-RHHS7HT4.js.map} +0 -0
- /package/dist/{alert-Z7V2ZUEW.js.map → alert-B5WEOKHP.js.map} +0 -0
- /package/dist/{api-catalog-UEDTRM6E.js.map → api-catalog-UXONX3BR.js.map} +0 -0
- /package/dist/{api-key-V5GEPRRG.js.map → api-key-J3A4TJKN.js.map} +0 -0
- /package/dist/{attachment-M7VZ6GSE.js.map → attachment-ZQW7DR4G.js.map} +0 -0
- /package/dist/{auth-DVTMWJKF.js.map → auth-HQGKUZBS.js.map} +0 -0
- /package/dist/{benchmark-KZJZYUTQ.js.map → benchmark-TIKJA32J.js.map} +0 -0
- /package/dist/{blackcat-QJXYDE66.js.map → blackcat-Z5LNV2GS.js.map} +0 -0
- /package/dist/{chunk-7MRZWPVC.js.map → chunk-3KBYI6RR.js.map} +0 -0
- /package/dist/{chunk-LPQTXBJF.js.map → chunk-3TZABC7D.js.map} +0 -0
- /package/dist/{chunk-ZOPVBINJ.js.map → chunk-5OKCT6LF.js.map} +0 -0
- /package/dist/{chunk-ZO5MSM4Q.js.map → chunk-635IQV6B.js.map} +0 -0
- /package/dist/{chunk-4EWSONHO.js.map → chunk-6X6ZTDXP.js.map} +0 -0
- /package/dist/{chunk-ZFN5FWN2.js.map → chunk-BJHAVM7Z.js.map} +0 -0
- /package/dist/{chunk-Q7UAEFW5.js.map → chunk-CQSQNDMV.js.map} +0 -0
- /package/dist/{chunk-AQOHNAEN.js.map → chunk-CZDQVQ57.js.map} +0 -0
- /package/dist/{chunk-CV22445U.js.map → chunk-EGWVL5XA.js.map} +0 -0
- /package/dist/{chunk-FLDIN6VX.js.map → chunk-JQG3V5E3.js.map} +0 -0
- /package/dist/{chunk-T6EQFICG.js.map → chunk-L4JQ236V.js.map} +0 -0
- /package/dist/{chunk-2M4PZK6Q.js.map → chunk-MB4FS35J.js.map} +0 -0
- /package/dist/{chunk-3YNR6U6Q.js.map → chunk-OU4I3MEH.js.map} +0 -0
- /package/dist/{chunk-HGEJHS45.js.map → chunk-QVMGXMP5.js.map} +0 -0
- /package/dist/{chunk-G2SCV2CY.js.map → chunk-R2CRH6JQ.js.map} +0 -0
- /package/dist/{chunk-HESV3F6U.js.map → chunk-RV7FZ4ID.js.map} +0 -0
- /package/dist/{chunk-7VBX2M4F.js.map → chunk-SCQVBLNR.js.map} +0 -0
- /package/dist/{chunk-74TU6PGO.js.map → chunk-TGI2AAOZ.js.map} +0 -0
- /package/dist/{chunk-L4Y5PZUE.js.map → chunk-W6DL4IP6.js.map} +0 -0
- /package/dist/{chunk-TRC63RSK.js.map → chunk-WWUEKQLR.js.map} +0 -0
- /package/dist/{chunk-MQQCUU5S.js.map → chunk-XM3WTJ7Y.js.map} +0 -0
- /package/dist/{chunk-YACDH6MJ.js.map → chunk-XWJJYZQC.js.map} +0 -0
- /package/dist/{chunk-QGP67JOK.js.map → chunk-YEH7UIUY.js.map} +0 -0
- /package/dist/{chunk-RA2NRAWU.js.map → chunk-YTXZUWMN.js.map} +0 -0
- /package/dist/{chunk-JAIMLQOU.js.map → chunk-Z72GWGH2.js.map} +0 -0
- /package/dist/{chunk-QYHJUHL7.js.map → chunk-ZH7HWVEE.js.map} +0 -0
- /package/dist/{chunk-IVWWYKAM.js.map → chunk-ZZZ33KTO.js.map} +0 -0
- /package/dist/{credential-546ER7WQ.js.map → credential-2IEZDTH3.js.map} +0 -0
- /package/dist/{credential-template-BWMEBGBN.js.map → credential-template-S5WSOI3P.js.map} +0 -0
- /package/dist/{digest-NIVBDDI2.js.map → digest-HQGL7H6E.js.map} +0 -0
- /package/dist/{docs-AK4MDP6K.js.map → docs-CJQZVERZ.js.map} +0 -0
- /package/dist/{doctor-NCLA47EL.js.map → doctor-EGK5AB77.js.map} +0 -0
- /package/dist/{environment-X3PV5IN2.js.map → environment-MWVXYEDU.js.map} +0 -0
- /package/dist/{eval-5FZKYZ6O.js.map → eval-F4NORBQI.js.map} +0 -0
- /package/dist/{exploration-2RVGIUKT.js.map → exploration-JC7R7KTJ.js.map} +0 -0
- /package/dist/{external-run-4NDVAHQB.js.map → external-run-UL5CKG54.js.map} +0 -0
- /package/dist/{feature-flag-C4IRE724.js.map → feature-flag-NMO25TZN.js.map} +0 -0
- /package/dist/{feedback-337OZCKC.js.map → feedback-3JNPSUXS.js.map} +0 -0
- /package/dist/{flow-D7ZLBKCH.js.map → flow-AYE4RTZN.js.map} +0 -0
- /package/dist/{github-LKXYIALH.js.map → github-DM2DCXRY.js.map} +0 -0
- /package/dist/{init-GKEC6LPH.js.map → init-CPDT5XAU.js.map} +0 -0
- /package/dist/{install-K45SAT2V.js.map → install-JZRNCHMO.js.map} +0 -0
- /package/dist/{issues-2HMTQYWR.js.map → issues-GRFJB52C.js.map} +0 -0
- /package/dist/{knobs-KUCGLO5Z.js.map → knobs-3Y4QXWAD.js.map} +0 -0
- /package/dist/{landing-KAET2I73.js.map → landing-ABYL5WVG.js.map} +0 -0
- /package/dist/{list-GDV4UYWV.js.map → list-WG5QB4NK.js.map} +0 -0
- /package/dist/{local-NFOSJLLV.js.map → local-KX24OR5Z.js.map} +0 -0
- /package/dist/{login-Y77HWROY.js.map → login-MLJQAIFK.js.map} +0 -0
- /package/dist/{mcp-CPCFMU6B.js.map → mcp-DDTO54XD.js.map} +0 -0
- /package/dist/{metrics-Y245ST63.js.map → metrics-BJLLOSPQ.js.map} +0 -0
- /package/dist/{monitor-BUDWSC3S.js.map → monitor-E2CKGHPM.js.map} +0 -0
- /package/dist/{perf-3JOAXPHP.js.map → perf-IX7JOMUE.js.map} +0 -0
- /package/dist/{product-CXL3YTMD.js.map → product-2ECWM2JN.js.map} +0 -0
- /package/dist/{product-map-656BTU74.js.map → product-map-O542LZQL.js.map} +0 -0
- /package/dist/{prompts-3SZ7O5E6.js.map → prompts-Y7O4XJ5P.js.map} +0 -0
- /package/dist/{proofs-4CSCCSNL.js.map → proofs-TNOBLSRP.js.map} +0 -0
- /package/dist/{property-IEKBXRVY.js.map → property-TZCGOZQG.js.map} +0 -0
- /package/dist/{psql-M4CM6TSI.js.map → psql-M66YSUXW.js.map} +0 -0
- /package/dist/{record-QP4TYZBS.js.map → record-3JINBRAR.js.map} +0 -0
- /package/dist/{redis-KG47IGJV.js.map → redis-FOIBINYA.js.map} +0 -0
- /package/dist/{release-QHPSQULK.js.map → release-B4VWMVEI.js.map} +0 -0
- /package/dist/{runbook-YVBG2QK3.js.map → runbook-EYMSE37E.js.map} +0 -0
- /package/dist/{scenarios-2SX2NUCD.js.map → scenarios-W7YNLBVO.js.map} +0 -0
- /package/dist/{schedule-P6F7MLXX.js.map → schedule-EDHQJUUZ.js.map} +0 -0
- /package/dist/{session-SZMCZTPE.js.map → session-4N62APNQ.js.map} +0 -0
- /package/dist/{slack-UDXTPOSW.js.map → slack-RUAXG6TB.js.map} +0 -0
- /package/dist/{spec-YCBBIV27.js.map → spec-ZZKMW7ER.js.map} +0 -0
- /package/dist/{src-ZKVNVLXY.js.map → src-CQ2YAW7F.js.map} +0 -0
- /package/dist/{src-DEGBASCD.js.map → src-DGIVA22K.js.map} +0 -0
- /package/dist/{src-DY7GLRO2.js.map → src-ECZWYNSV.js.map} +0 -0
- /package/dist/{src-NG7DRE5A.js.map → src-OKSAEHQS.js.map} +0 -0
- /package/dist/{start-ZYZSORPY.js.map → start-OCZGILRL.js.map} +0 -0
- /package/dist/{status-ESIG6A7W.js.map → status-YTJ6YITG.js.map} +0 -0
- /package/dist/{tag-3K5Y25AR.js.map → tag-KBG4SWRL.js.map} +0 -0
- /package/dist/{task-JLNX7O3M.js.map → task-O6XRMHYE.js.map} +0 -0
- /package/dist/{teams-F4IU3EXE.js.map → teams-NFUHS7XI.js.map} +0 -0
- /package/dist/{test-suite-QHDUW5W5.js.map → test-suite-6BH3DESN.js.map} +0 -0
- /package/dist/{tickets-NBL5WACL.js.map → tickets-JU2STK4B.js.map} +0 -0
- /package/dist/{trace-ZKYAI5JF.js.map → trace-NMSGKASJ.js.map} +0 -0
- /package/dist/{triage-5NWCREUM.js.map → triage-JGECJJU5.js.map} +0 -0
- /package/dist/{trigger-KXYYY5IR.js.map → trigger-OSPEVCCQ.js.map} +0 -0
- /package/dist/{update-check-Q2URVJLC.js.map → update-check-FNP6RFZO.js.map} +0 -0
- /package/dist/{upgrade-4XSASC7U.js.map → upgrade-3PPBXFAT.js.map} +0 -0
- /package/dist/{usage-UY7VZYVY.js.map → usage-QMCU2JIO.js.map} +0 -0
- /package/dist/{utilities-53NXNLA7.js.map → utilities-7VHWTSJT.js.map} +0 -0
- /package/dist/{visual-IJAT7X7D.js.map → visual-WIZSPTVK.js.map} +0 -0
- /package/dist/{workflow-cache-FP6MKLVR.js.map → workflow-cache-ACGM3BOI.js.map} +0 -0
- /package/dist/{workflow-cache-convergence-resolve-HS5KO7US.js.map → workflow-cache-convergence-resolve-KEQDQMKL.js.map} +0 -0
- /package/dist/{workflow-debug-EM55H2QE.js.map → workflow-debug-TVLBNRCB.js.map} +0 -0
- /package/dist/{workflow-feedback-GNWNWVHH.js.map → workflow-feedback-GB5VPGE2.js.map} +0 -0
- /package/dist/{workflow-fork-M5AQWST7.js.map → workflow-fork-2IPJNR76.js.map} +0 -0
- /package/dist/{workflow-promotion-HYULZDYL.js.map → workflow-promotion-QEQUXVUT.js.map} +0 -0
- /package/dist/{workflow-proposals-EN327FSE.js.map → workflow-proposals-KHGTPMD4.js.map} +0 -0
- /package/dist/{workflow-revisions-ZMTVNWRW.js.map → workflow-revisions-F5KQEEXB.js.map} +0 -0
- /package/dist/{workflow-status-FNAJG34W.js.map → workflow-status-66HC5MEA.js.map} +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { createRequire as __cr } from "module"; import { fileURLToPath as __fup } from "url"; import { dirname as __dn } from "path"; const require = __cr(import.meta.url); const __filename = __fup(import.meta.url); const __dirname = __dn(__filename);
|
|
2
2
|
import {
|
|
3
3
|
getGuideSectionArg
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WWUEKQLR.js";
|
|
5
5
|
import {
|
|
6
6
|
hasFlag,
|
|
7
7
|
resolveApiUrl
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-BJHAVM7Z.js";
|
|
9
9
|
import {
|
|
10
10
|
EVAL_GUIDE_SECTIONS,
|
|
11
|
+
EXPLORATORY_TEST_GUIDE_SECTIONS,
|
|
11
12
|
EXTERNAL_RUN_GUIDE_SECTIONS,
|
|
12
13
|
ISSUES_GUIDE_SECTIONS,
|
|
13
14
|
PERF_GUIDE_SECTIONS,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
command,
|
|
18
19
|
stripControlCharacters,
|
|
19
20
|
version
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-KL2ARGH6.js";
|
|
21
22
|
|
|
22
23
|
// ../content/src/sandbox-v2-guide/support.ts
|
|
23
24
|
var SANDBOX_V2_SUPPORT_MATRIX = [
|
|
@@ -291,7 +292,7 @@ var seeds_and_actions_default = "An action is a script with a typed input and ou
|
|
|
291
292
|
var data_fast_forward_default = "A database dump keeps the dates of the day it was taken. Restore it into a\nsandbox months later and every trial, token and scheduled item in it has\nalready expired, so the app behaves like a dead system. The built-in\noperation `database.fast-forward` moves a restored Postgres database's\ndates forward to the moment the snapshot was built, so the data looks\nfreshly captured. It changes data only; the sandbox's clock is already\ncorrect (see \"WHAT IS NOT FAKED\" below).\n\nDECLARE THE DATABASE\n\n A top-level `databases` block names each database and the date its dump\n was captured. Like every top-level field, it works in GitHub-delivered\n and pushed definitions alike.\n databases:\n app:\n engine: postgres\n url: ${env.DATABASE_URL}\n as_of: 2026-09-30T00:00:00Z\n\n `engine` `postgres` is the only engine today.\n `url` Exactly one `${env.NAME}` reference and nothing else around\n it (no `postgres://${env.HOST}/db`). It is resolved inside the\n sandbox, against the env an action sees: the definition's\n `env` (with `${params.*}` filled in), the environment's `env`,\n the emulators' env and repository secrets. So a `DATABASE_URL` in `env`, or a\n secret of that name, works, and no credential is committed.\n `as_of` When the dump was captured, in UTC with a trailing `Z`. A date\n without a time, or a local offset, is rejected by `validate`.\n\nADD THE STEP AFTER THE RESTORE\n\n A seed step runs the operation with `operation:` instead of `action:`\n (see `seeds-and-actions`). It must come after the step that restores\n the dump:\n environments:\n default:\n description: A customer database restored from a 2026-09-30 dump,\n then moved forward to the snapshot's build time.\n seeds:\n - { id: restore, action: restore-dump }\n - { id: fast_forward, operation: database.fast-forward, input: { database: app } }\n\n `input.database` is a key of `databases`; `validate` reports an unknown\n one with its path. A restore that recreates the database also clears\n the record of earlier fast-forwards (below), which is right: the data is\n old again. A restore that only replaces tables must also run\n `drop schema if exists cofactor cascade`, or a later fast-forward\n starts from the old record and moves nothing (the worked example's\n dump does this).\n\nWHEN IT RUNS IS WHERE YOU PUT IT\n\n There is no mode flag. Placement is the \"when\":\n - As a seed step it runs once, while the version's snapshot is built,\n so every claim of that snapshot starts with shifted data.\n - On a live sandbox, run the same operation by hand:\n cofactor sandbox run <id> database.fast-forward --input database=app\n It shifts from the date the database recorded last time (below), so\n a sandbox kept for days can be brought current again.\n\nTHE SHIFT RULE\n\n - `as_of` is the anchor. The data moves by the whole days between its\n current date and now, rounded down, so it ends at most a day behind\n now and never in the future. Times of day are kept: a row at 14:30\n still reads 14:30.\n - The first run starts from `as_of`. When it finishes it records the\n data's new date in the database itself, in the table\n `cofactor.fast_forward` (its own schema, so your migration tool does\n not see it). Later runs start from that date instead of `as_of`, and\n the record travels with the data into every snapshot and copy.\n - If less than a day has passed the run changes no table (`offset_days:\n 0`). An `as_of` in the future fails.\n\nWHAT IS SHIFTED\n\n Every `timestamp`, `timestamptz` and `date` column of every base table\n in every schema except the system ones and `cofactor`. `infinity`\n values stay `infinity`.\n - A generated column is recomputed from its shifted inputs.\n - A partitioned table is updated through its parent, so rows move\n between partitions as their dates move.\n - User triggers are disabled while a table is shifted, so a trigger\n such as `updated_at = now()` leaves `updated_at` with its shifted\n value. Afterwards every user trigger on a touched table is enabled\n again, including one your dump had deliberately disabled.\n\nWHAT IS NOT SHIFTED\n\n Integer epoch columns, dates inside JSON, dates stored as text, columns\n of a domain over a date type, array columns, and materialized views.\n Refresh materialized views in a step after the fast-forward:\n - { id: refresh, action: refresh-views }\n\nREQUIREMENTS AND FAILURES\n\n - The role in `url` must own the tables (it is normally the role that\n restored the dump). A role that does not fails with Postgres's own\n \"must be owner of table\" error.\n - The whole shift is one transaction. Any error rolls everything back,\n so nothing is half-shifted, and the error names the table (and the\n constraint, when there is one):\n table public.sessions, constraint sessions_day_key: duplicate key ...\n - A unique constraint over a date column can collide while its rows are\n moving; that fails the run. A foreign key whose columns are shifted\n dates is checked as each table moves and can fail the run with\n Postgres's own error.\n - On a live sandbox the shift locks every table that has a date column\n until it commits. Run it when the app is idle.\n - After a large dump, follow the step with an action that runs\n `VACUUM (ANALYZE)`: the update leaves dead rows and stale statistics\n behind.\n\nWHAT IS NOT FAKED\n\n The sandbox's system clock is not changed, and does not need to be: it\n is correct after every claim. Only data moves. So state minted in memory\n while the snapshot was built (sessions, tokens an app created during a\n seed) is as old as the snapshot. A claim's data is as old as its\n snapshot plus under a day of rounding, and a snapshot is rebuilt on\n every push, so keep pushing (a nightly push is plenty). A preview moved\n in place to a new commit keeps its data and is not shifted again.\n\nOUTPUT\n\n The step (and a `sandbox run`) returns:\n database the key in `databases`\n offset_days whole days the data moved this run (0: nothing to do)\n tables, rows how many tables and rows were updated\n from, to the data's date before and after (ISO-8601, UTC)\n A later step can use them, e.g. `${fast_forward.offset_days}`.\n `cofactor sandbox show` lists each seed step with its time and prints\n these under the seed outputs.\n\nWORKED EXAMPLE\n\n `packages/demo-app-postgres/cofactor.sandbox.yaml` in TreebeardHQ/qa is\n a complete Postgres app: a dump from 2026-09-30, a restore action, the\n `databases` block and the two-step seed above, plus an `aged`\n environment that rewinds the data and fast-forwards it a second time.\n";
|
|
292
293
|
|
|
293
294
|
// ../content/src/sandbox-v2-guide/personas.txt
|
|
294
|
-
var personas_default = "A persona is a named login identity declared in `cofactor.sandbox.yaml`:\n personas:\n owner: { email: owner@example.test }\n member: { email: member@example.test }\n viewer: { email: viewer@example.test }\n\nDeclare one per role your tests need (see \"WHAT TO BUILD FIRST\" in\n`seeds-and-actions`). Only the email is fixed in the definition.\nEverything else about a persona is assigned per sandbox or per seed:\n\nPASSWORDS \u2014 SET WHEN THE SNAPSHOT IS BUILT\n\n Each ready snapshot gets a randomly generated password per persona,\n created by the snapshot's own `prepare` (see the `overview` topic); every\n copy claimed from that snapshot has the same passwords. Those passwords\n never appear in the definition \u2014 they're generated fresh for the snapshot\n and handed to\n `lifecycle.prepare` as `COFACTOR_PERSONAS`, a JSON map of persona name to\n `{email, password}`. Your prepare script reads it and makes each login\n work in your app: the example app's `sandbox/personas.sh` creates or\n updates one user per persona (email + password hash), with no workspace\n and no role \u2014 just the login itself. The `upload` topic covers the\n details (prepare runs while the app is up; logins must be read per\n sign-in, not once at startup).\n\n A copy's `lifecycle.prepare` still runs, with the snapshot's unchanged\n personas, so make the persona step cheap when nothing changed (skip\n hashing a password the user already has): the example app's personas.sh\n skips when a digest of COFACTOR_PERSONAS and of the users' stored hashes\n both match what its last run left, so a reset or replaced user still\n takes the full path.\n\n If your app gates sign-in behind a first-run step (accepting terms,\n initializing the instance, verifying the email address, finishing\n onboarding), clear it in prepare for each persona, the way you set the\n password: a seed or test signing in through the real login API should\n land signed in, not on the gate.\n\nROLES AND MEMBERSHIPS \u2014 SET BY SEEDS\n\n A persona existing (having a login) is separate from what it can do.\n Workspace membership and role are set by seed steps, not by the\n definition: `create-workspace` makes its `as` persona the owner of the\n new workspace, and `add-member` gives a persona a role in it. This split\n matters: the same persona can be \"just a login\" in one environment and\n a workspace owner in another, because that comes from which seeds\n actually ran. If a persona's email is an account your app already has\n with the right role, prepare setting its password is enough.\n\nHOW A SEED OR ACTION SIGNS IN AS A PERSONA\n\n Actions receive `COFACTOR_PERSONAS` too. An action that performs a real\n operation takes the acting persona as an input (`as`, of type\n `persona`, which arrives as `INPUT_AS` holding the persona's name),\n reads that persona's email and password from `COFACTOR_PERSONAS`, and\n signs in through the app's real login endpoint \u2014 the same way a user\n would \u2014 keeping the session cookie for its next calls:\n login=$(jq -c --arg p \"$INPUT_AS\" '.[$p] | {email, password}' <<<\"$COFACTOR_PERSONAS\")\n jar=$(mktemp)\n curl -fsS -c \"$jar\" -H 'content-type: application/json' \\\n -d \"$login\" \"$API_URL/api/auth/login\" >&2\n See the `seeds-and-actions` topic on why `as` is never assumed.\n\n An action runs inside the sandbox, so it can reach the app on\n localhost. If your app's session cookie is `Secure` (or `__Host-`\n prefixed) and your HTTP client won't send it back over plain http, call\n the app through its public port URL (here `API_URL`, from a\n `{port: api}` param) instead.\n\n Keep whatever the login returns that later calls need: the session\n cookie, a bearer token from the response body, or both (some apps bind\n a bearer token to an httpOnly cookie and refuse one without the other).\n\n Every action signs in from the same address, so a seed that runs several\n actions can trip the app's login rate limit (often around 10 attempts a\n minute). If your app has one, raise it for the sandbox through the\n definition's top-level `env:` (it reaches every service) rather than\n adding retries to your actions.\n\nWHERE LOGINS ARE SHOWN\n\n Because copies of a snapshot share persona passwords, anyone who has\n claimed one can sign in to another copy of the same snapshot whose URL\n they learn; keep sensitive data out of sandboxes.\n\n Persona logins (email + generated password) are surfaced by\n `cofactor sandbox show <id>` and by `cofactor sandbox
|
|
295
|
+
var personas_default = "A persona is a named login identity declared in `cofactor.sandbox.yaml`:\n personas:\n owner: { email: owner@example.test }\n member: { email: member@example.test }\n viewer: { email: viewer@example.test }\n\nDeclare one per role your tests need (see \"WHAT TO BUILD FIRST\" in\n`seeds-and-actions`). Only the email is fixed in the definition.\nEverything else about a persona is assigned per sandbox or per seed:\n\nPASSWORDS \u2014 SET WHEN THE SNAPSHOT IS BUILT\n\n Each ready snapshot gets a randomly generated password per persona,\n created by the snapshot's own `prepare` (see the `overview` topic); every\n copy claimed from that snapshot has the same passwords. Those passwords\n never appear in the definition \u2014 they're generated fresh for the snapshot\n and handed to\n `lifecycle.prepare` as `COFACTOR_PERSONAS`, a JSON map of persona name to\n `{email, password}`. Your prepare script reads it and makes each login\n work in your app: the example app's `sandbox/personas.sh` creates or\n updates one user per persona (email + password hash), with no workspace\n and no role \u2014 just the login itself. The `upload` topic covers the\n details (prepare runs while the app is up; logins must be read per\n sign-in, not once at startup).\n\n A copy's `lifecycle.prepare` still runs, with the snapshot's unchanged\n personas, so make the persona step cheap when nothing changed (skip\n hashing a password the user already has): the example app's personas.sh\n skips when a digest of COFACTOR_PERSONAS and of the users' stored hashes\n both match what its last run left, so a reset or replaced user still\n takes the full path.\n\n If your app gates sign-in behind a first-run step (accepting terms,\n initializing the instance, verifying the email address, finishing\n onboarding), clear it in prepare for each persona, the way you set the\n password: a seed or test signing in through the real login API should\n land signed in, not on the gate.\n\nROLES AND MEMBERSHIPS \u2014 SET BY SEEDS\n\n A persona existing (having a login) is separate from what it can do.\n Workspace membership and role are set by seed steps, not by the\n definition: `create-workspace` makes its `as` persona the owner of the\n new workspace, and `add-member` gives a persona a role in it. This split\n matters: the same persona can be \"just a login\" in one environment and\n a workspace owner in another, because that comes from which seeds\n actually ran. If a persona's email is an account your app already has\n with the right role, prepare setting its password is enough.\n\nHOW A SEED OR ACTION SIGNS IN AS A PERSONA\n\n Actions receive `COFACTOR_PERSONAS` too. An action that performs a real\n operation takes the acting persona as an input (`as`, of type\n `persona`, which arrives as `INPUT_AS` holding the persona's name),\n reads that persona's email and password from `COFACTOR_PERSONAS`, and\n signs in through the app's real login endpoint \u2014 the same way a user\n would \u2014 keeping the session cookie for its next calls:\n login=$(jq -c --arg p \"$INPUT_AS\" '.[$p] | {email, password}' <<<\"$COFACTOR_PERSONAS\")\n jar=$(mktemp)\n curl -fsS -c \"$jar\" -H 'content-type: application/json' \\\n -d \"$login\" \"$API_URL/api/auth/login\" >&2\n See the `seeds-and-actions` topic on why `as` is never assumed.\n\n An action runs inside the sandbox, so it can reach the app on\n localhost. If your app's session cookie is `Secure` (or `__Host-`\n prefixed) and your HTTP client won't send it back over plain http, call\n the app through its public port URL (here `API_URL`, from a\n `{port: api}` param) instead.\n\n Keep whatever the login returns that later calls need: the session\n cookie, a bearer token from the response body, or both (some apps bind\n a bearer token to an httpOnly cookie and refuse one without the other).\n\n Every action signs in from the same address, so a seed that runs several\n actions can trip the app's login rate limit (often around 10 attempts a\n minute). If your app has one, raise it for the sandbox through the\n definition's top-level `env:` (it reaches every service) rather than\n adding retries to your actions.\n\nWHERE LOGINS ARE SHOWN\n\n Because copies of a snapshot share persona passwords, anyone who has\n claimed one can sign in to another copy of the same snapshot whose URL\n they learn; keep sensitive data out of sandboxes.\n\n Persona logins (email + generated password) are surfaced by\n `cofactor sandbox show <id>` and, for PR previews, by `cofactor sandbox\n preview` and the preview's Cofactor page (`/sandboxes/previews/<application\n id>/<pr number>`, org members signed in only). They are never sent to\n GitHub \u2014 the PR's deployment only links that page, keeping generated\n passwords out of anything that ends up in GitHub's own history.\n Cofactor's test runs sign in with them automatically, as the\n application's persona of the same name.\n\n A person who claims a sandbox signs in the ordinary way: open its URL and\n use your app's own login with the email and password the claim prints.\n There is no CLI command that hands you a signed-in browser session;\n `authentication.run` (below) is only for Cofactor's automated test runs.\n\n\nGOOGLE ACCOUNTS\n\n When the definition declares the `google-oauth` emulator, a persona's\n email is also its Google identity: the Google account chooser lists each\n persona, and `authentication.emulated` signs a persona in through Google\n as that email. `google-oauth` needs at least one persona. See\n `cofactor sandbox guide v2 authentication`.\n\n\nNATIVE WORKFLOW LOGIN NODES\n\n Sandboxes v2 uses `authentication` (`run`, or `emulated` for Google\n sign-in) to mint a fresh browser session for the persona named by each\n native workflow login node. Read\n `cofactor sandbox guide v2 authentication` for the complete script contract,\n cookie and localStorage formats, and how to keep provider-login journeys\n clean and unauthenticated at the start.\n";
|
|
295
296
|
|
|
296
297
|
// ../content/src/sandbox-v2-guide/authentication.txt
|
|
297
298
|
var authentication_default = "This script is only for Cofactor's automated test runs: a person who claims a sandbox signs in through your app's own login with the email and password the claim prints, and only `cofactor sandbox verify-env` runs it outside a test (it signs every persona in on its claim to prove the script works). Native workflow login nodes use the persona declared in this sandbox definition. Declare `authentication` when your sandbox needs native persona login: exactly one of `run` (your script) or `emulated` (Cofactor signs the persona in through the Google or OIDC emulator; see GOOGLE SIGN-IN AS A PERSONA and STANDARD OIDC below). Existing definitions can use personas in prepare, seeds and actions without this script; a native login without it fails with `script_not_declared`. Each run gets its own copy, so Cofactor asks the copy's `authentication.run` script to create an application session for the requested persona. The script receives the prepared copy environment and `COFACTOR_AUTH_PERSONA`; it returns Playwright storage state as JSON on stdout. The cookies must be for the copy's public origin (sign in through the public URL, as below), not localhost. The example needs no language runtime: a POSIX shell, `curl`, `jq` and `awk`:\n\n```yaml\npersonas:\n admin: { email: admin@example.test }\n member: { email: member@example.test }\nauthentication:\n run: sandbox/auth.sh\n```\n\n```bash\n#!/bin/sh\nset -eu\n\npersona=${COFACTOR_AUTH_PERSONA:?missing persona}\n# This persona's email and generated password; fails if either is missing.\nlogin=$(printf '%s' \"$COFACTOR_PERSONAS\" | jq -ec --arg p \"$persona\" \\\n '.[$p] | {email, password} | select((.email | type) == \"string\" and (.password | type) == \"string\")')\nemail=$(printf '%s' \"$login\" | jq -r .email)\njar=$(mktemp)\ntrap 'rm -f \"$jar\"' EXIT\n\n# Sign in through the copy's public HTTPS API. This preserves Secure and\n# __Host- cookie behavior; localhost HTTP is not suitable for those cookies.\nprintf '%s' \"$login\" | curl -fsS -c \"$jar\" -H 'content-type: application/json' \\\n --data-binary @- \"$API_URL/api/auth/login\" >/dev/null\n\n# Check the resulting identity before giving the session to the browser.\ncurl -fsS -b \"$jar\" \"$API_URL/api/auth/me\" | jq -e --arg email \"$email\" '.user.email == $email' >/dev/null\n\n# Convert the cookie jar to Playwright storage-state JSON. Curl's Netscape\n# format does not carry SameSite, so adapt this field to your app's cookie.\nawk -F '\\t' '\n { httponly = \"false\" }\n /^#HttpOnly_/ { sub(/^#HttpOnly_/, \"\"); httponly = \"true\" }\n /^#/ || NF < 7 { next }\n { print $6 \"\\t\" $7 \"\\t\" $1 \"\\t\" $3 \"\\t\" $5 \"\\t\" httponly \"\\t\" $4 }\n' \"$jar\" | jq -Rn '{\n cookies: [inputs | split(\"\\t\") | {\n name: .[0], value: .[1], domain: .[2], path: .[3], expires: (.[4] | tonumber | if . == 0 then -1 else . end),\n httpOnly: (.[5] == \"true\"), secure: (.[6] == \"TRUE\"), sameSite: \"Lax\"\n }],\n origins: []\n}'\n```\n\nIf the app stores its access token in local storage, include\nan `origins` entry for the exact copy origin, with `{name, value}` pairs in\n`localStorage`. Do not print secrets to stderr, write state to a normal action,\nor use a fixed browser profile. Cofactor validates the returned state against\nthe current copy's public origins and keeps it private to that execution.\n\nWHEN THE SCRIPT FAILS\n\nWrite diagnostics to stderr: stdout is the state and is never shown. When the\nscript exits non-zero (or runs past its 60 s limit), the Login step fails with\nwhich persona, the exit code and the end of the script's stderr, for example:\n\n sandbox authentication failed: script_failed (persona \"admin\": sandbox/auth.sh exited 127). Its stderr ended:\n sandbox/auth.sh: line 4: curl: command not found\n\nEvery org member can read that error. Only the copy's known values (secrets,\nsecret params, persona passwords) are scrubbed from it, verbatim: a session\ncookie, token or login response the script prints is shown as is. So print\nwhat failed, never a response body, `curl -v` or `set -x` trace.\n\nExit 127 is a command the image does not have: install it in the image, or\nuse what it has. `cofactor sandbox verify-env` signs every persona in on a\nfresh claim and reports the same message per persona, so run it after every\npush that changes the image or the script, before a test depends on it.\n\nTHIRD-PARTY IDENTITY PROVIDERS\n\nFor ordinary application tests, create the app's session directly. Use the\nsandbox-only login API, a signed test token accepted only by the copy, an SDK,\nor the provider's emulator-backed token exchange. The auth action should\nfinish with the application's own session cookie or local-storage token. A\nflow that tests login itself must begin clean: retain its application login\nnode with `performLogin: false` to select the app and environment, without\nseeding or preloading an app session. Then navigate to the app and traverse\nits real sign-in UI against the declared provider emulator.\n\nGoogle OAuth requires interception on both sides. Cofactor intercepts the\nbrowser authorization request, including an app HTTP redirect to Google,\nand answers it from the emulator: its account chooser, or a code for the\ncopy's callback when the request's login_hint matches a persona. The\ndeclared sandbox emulator handles the app's token exchange and user-info\nrequests. Verify the complete callback and resulting app identity; an\nintercepted browser redirect alone does not prove the server handshake\nworks.\n\nGOOGLE SIGN-IN AS A PERSONA (authentication.emulated)\n\n When the app's only login is \"Sign in with Google\", there is no password\n endpoint for a script to call. Declare `emulated` instead of `run`, and\n Cofactor signs the persona in itself, inside the copy, with no script:\n authentication:\n emulated:\n provider: google-oauth\n sign_in: ${params.web_url}/sign-in\n\n `authentication` is exactly one of `run` or `emulated`; both, neither key\n inside it, or an unknown key fails validation. `provider` must be\n `google-oauth` or `oidc` (this section describes `google-oauth`; for `oidc`\n see STANDARD OIDC below) and must be declared under `emulation`. `sign_in` is the app URL that starts the Google\n sign-in: `${params.<name>}/path`, where the param is a `{port: ...}`\n param on a PUBLIC port, because the sign-in starts on the copy's public\n origin. Use the port that serves your sign-in route, for example\n `${params.api_url}/auth/google` when the API starts the flow and the\n web app only links to it, or `${params.web_url}/sign-in` for a single-port\n app. A bare path, an undeclared param or a private-port param fails\n validation.\n\n A complete definition that passes `cofactor sandbox validate`:\n\n version: 1\n resources: { vcpus: 2, memory_mib: 4096 }\n setup: |\n apt-get update -qq\n apt-get install -y -qq curl unzip >/dev/null\n curl -fsSL https://bun.sh/install | bash >/dev/null\n ports:\n web: { port: 8300, access: public }\n params:\n web_url: { port: web }\n env:\n PUBLIC_URL: ${params.web_url}\n GOOGLE_CLIENT_ID: sandbox\n GOOGLE_CLIENT_SECRET: sandbox\n GOOGLE_REDIRECT_URI: ${params.web_url}/auth/callback\n services:\n app:\n run: bun server.ts\n health: { http: 8300, path: /health }\n reload_on_prepare: true\n personas:\n alice: { email: alice@example.test }\n bob: { email: bob@example.test }\n authentication:\n emulated:\n provider: google-oauth\n sign_in: ${params.web_url}/sign-in\n emulation:\n google-oauth: {}\n lifecycle:\n build:\n - name: install\n run: bun install\n inputs: [bun.lock, package.json]\n prepare: []\n environments:\n default: { description: Stock data. }\n\n `setup` is for GitHub-delivered definitions, as in the `definition` topic; a\n pushed image omits it (it is already built).\n\n What happens for a Login step as persona `bob`:\n 1. The helper GETs `sign_in` on the copy's public URL and follows the\n redirects with a cookie jar (at most 10).\n 2. At the hop to accounts.google.com, which resolves to the emulator\n inside the VM, it adds `login_hint=bob@example.test`. The emulator\n completes the authorize request for that persona without showing\n the chooser.\n 3. The emulator redirects to the app's callback; the app exchanges the\n code and sets its session cookie.\n 4. The cookies for the copy's public origin become the Playwright\n storage state, validated like the output of `authentication.run`.\n\n Every redirect hop must stay on the origins of your public `{port}`\n params or on accounts.google.com; a hop anywhere else fails the\n sign-in. Success needs the provider hop AND a cookie set for the copy\n after it, so a pre-provider CSRF or state cookie is not mistaken for a\n session. The\n Login node, persona switching and landing check are the same as for\n `authentication.run`. A failed emulated sign-in shows the Login step\n error `sandbox authentication failed: script_failed (persona \"bob\": the\n sandbox helper reported <reason>)`, where the reason is one of\n `sign_in_unreachable`, `sign_in_rejected`, `sign_in_no_session`,\n `callback_origin` or `invalid_state`. Look for what a\n customer can fix: the app redirects to an origin the definition does not\n declare as a public `{port}` param; `sign_in` never reaches Google; the\n service that calls Google lacks `reload_on_prepare` or does not trust\n /etc/emulation/ca.crt. The Login step can also fail with\n `state_origin_mismatch` when the app sets a cookie for a parent Domain\n outside the copy's public origins. The specific failure category is\n visible to Cofactor support, and the emulator's own log is in the copy at\n /var/lib/cofactor/logs/emulator.log.\n\n Emulated sign-in covers sign-ins the server starts with HTTP redirects.\n It does not cover Google One Tap, FedCM or popup sign-ins done in\n browser JavaScript, and it does not cover a person using the copy in\n their own browser: accounts.google.com is hard-coded in apps and only\n Cofactor's browser (and the VM itself) reaches the emulator for it.\n\n The chooser. When Cofactor's browser agent reaches Google's authorize\n URL, the emulator serves a real account-chooser page listing every\n persona by name and email; clicking one completes the sign-in as that\n persona. A flow that tests the Google journey itself clicks \"Sign in with\n Google\", picks a persona on the chooser, and asserts the signed-in\n identity.\n\n `login_hint` preselects. An authorize request whose `login_hint` equals a\n persona's email (case-insensitive) skips the chooser. This holds even\n when the app also sends `prompt=select_account`, which real Google would\n honor by showing its chooser; that is a deliberate deviation so the\n helper and hint-sending apps sign in without a click. An absent or\n unmatched hint shows the chooser.\n\n Emulator limits. Authorization codes are single use, and are bound to the\n `redirect_uri` of the authorize request, and to its `client_id` when both\n the authorize request and the token request body carry client_id: a\n replayed code, a code the emulator did not issue, or a mismatch is\n `invalid_grant`. Send the same `redirect_uri` on authorize and on the token exchange, as real Google\n requires. The emulator does NOT check that `redirect_uri` is registered\n anywhere; any http(s) URL is accepted, so a wrong callback is caught only\n by your app.\n\nGOOGLE CALLBACK CONFIGURATION\n\n Supply the current copy's callback using the app's existing configuration:\n params:\n api_url: { port: api }\n env:\n GOOGLE_CLIENT_ID: sandbox\n GOOGLE_CLIENT_SECRET: sandbox\n GOOGLE_REDIRECT_URI: ${params.api_url}/auth/callback\n emulation:\n google-oauth: {}\n personas:\n alice: { email: alice@example.test }\n\n This is a fragment: declare the public api port, use your app's actual\n variable names and callback path, and set reload_on_prepare: true on the\n backend calling Google. An app whose callback comes from configuration\n can use this without auth code changes. A hardcoded production callback\n must become configurable. No per-copy Google Console registration is\n needed for the emulated path; real Google login keeps Google's rules.\n\n Do not add live: true merely to avoid service restarts. That optimization\n requires the app to re-read the live value per request; a normal {port}\n param with reload_on_prepare works for apps that read env at startup.\n\n Google needs both browser authorization interception and sandbox-side\n token/user-info emulation. Verify the app callback and a protected request,\n not just that the browser received an emulated code.\n\n Each persona is a Google account: its email is the Google email, and its\n Google `sub` is derived from the persona name, so it is the same in every\n copy. The identity claims are `sub`, `email`, `email_verified`, `name` and\n `given_name` (both the persona name), plus the id_token's standard\n iss/aud/azp/nonce and time claims. No `family_name`, `picture` or `hd`.\n There is no default Google user; a\n definition that declares `google-oauth` with no personas fails validation.\n\n If Google is reached through hosted Auth0, Clerk or another intermediary,\n this Google emulator does not emulate that intermediary. If the app lets\n you configure its own OIDC provider (an issuer or authority setting), use\n the `oidc` emulator instead: see STANDARD OIDC below. Otherwise inspect where\n token exchange occurs. Prove the customer's actual library and trust setup;\n do not disable token verification or alter production auth to pass a test.\n Non-Node clients must trust /etc/emulation/ca.crt through their runtime's\n supported mechanism. This native Login-node contract does not inject\n authentication into an external Playwright suite.\n\nSTANDARD OIDC (Auth0, Okta, Cognito, Keycloak, Clerk, ...)\n\n For an app that signs in through any standard OpenID Connect provider,\n declare the `oidc` emulator and point the app's issuer at it. The emulator\n is a generic OIDC provider, not a copy of one vendor: use it with the\n vendor's own SDK or any OIDC library, as long as the app's issuer (or\n authority, domain or base URL) setting is configurable.\n\n This is a fragment: declare the public web port, use your app's actual\n variable names and callback path, and set reload_on_prepare: true on the\n service that reads the issuer.\n params:\n web_url: { port: web }\n issuer_url: { port: emulation }\n env:\n OIDC_ISSUER: ${params.issuer_url}\n OIDC_CLIENT_ID: sandbox\n OIDC_CLIENT_SECRET: sandbox\n OIDC_REDIRECT_URI: ${params.web_url}/auth/callback\n services:\n app:\n run: bun server.ts\n health: { http: 8300, path: /health }\n reload_on_prepare: true\n personas:\n alice: { email: alice@example.test }\n emulation:\n oidc: {}\n\n `issuer_url: { port: emulation }` is the copy's public emulator address and\n is the issuer, exactly: an https origin with no trailing slash, with a real\n TLS certificate. The app needs no CA and no /etc/hosts entry, and a\n library that refuses an http issuer is satisfied. Point the vendor SDK's\n issuer, authority or domain setting at it (`OIDC_ISSUER` above); the SDK\n discovers everything else from\n `<issuer>/.well-known/openid-configuration`. The service that reads the\n issuer needs `reload_on_prepare: true`, because the address is per copy.\n\n Endpoints: /.well-known/openid-configuration, /authorize, /token,\n /userinfo (GET and POST), /jwks.\n\n Client id and secret are accepted as-is: use any values, such as\n `sandbox`. The token endpoint takes client credentials in the body or by\n HTTP Basic and never checks the secret. PKCE is advertised (S256 and\n plain) so libraries that send it proceed, but the challenge is not\n verified.\n\n Redirect rule. The `redirect_uri` on /authorize must be an http(s) URL on\n one of the copy's public origins (any public `{port}` param of the\n definition); anything else is a 400 and nothing is redirected. Use the\n port that serves the callback, as in GOOGLE CALLBACK CONFIGURATION.\n\n Each persona is an account: its email is the account email. Claims, in the\n id_token and from /userinfo, are `sub` (stable per persona, the same in every\n copy), `email`, `email_verified` (true) and `name`, plus the id_token's\n standard iss/aud/azp/nonce and time claims. Nothing else: no groups, roles,\n `picture` or vendor-specific claims, so an app that depends on those needs\n its own handling in the sandbox. Authorization codes are single use and are\n issued only by /authorize; a replayed or never-issued code is `invalid_grant`,\n as is a different `redirect_uri`. The id_token is signed RS256 with a key\n published at /jwks.\n\n The sign-in link. /authorize is locked: signing in needs a browser that has\n opened the copy's sign-in link. Without it, /authorize shows a page saying to\n open the sandbox's sign-in link from Cofactor (HTTP 403) and the app is not\n signed in. The link is `<emulation origin>/oidc/enter?key=...`. Opening it\n sets a browser-session cookie and shows \"Sign-in is unlocked\"; after that the\n app's Sign in button reaches the account chooser, which lists each persona by\n name and email, and picking one signs in as that persona. The unlock lasts\n until that browser session ends; the link itself works for the copy's life,\n so reopen it if you land on the locked page again. `cofactor sandbox claim` and `cofactor sandbox show <id>`\n print the link as `Open sign-in: ...` next to the persona logins, and the\n sandbox API returns it as `signInUrl`. Only organization members who can read\n sandboxes see it. A preview has no sign-in link. Treat the link like the\n persona passwords: it is a credential for that copy. `--json` output includes\n it, so keep that output out of CI logs.\n\n Cofactor's own browser agent and native Login steps pass the gate\n automatically, so a flow that tests the sign-in journey needs no link step.\n\n Login steps. `authentication.emulated` with `provider: oidc` signs a persona\n in with no script:\n authentication:\n emulated:\n provider: oidc\n sign_in: ${params.web_url}/login\n\n `oidc` must be declared under `emulation`, and `sign_in` follows the same\n rules as for Google (`${params.<name>}/path` on a public `{port}` param).\n Inside the copy the helper GETs `sign_in`, follows the redirects with a cookie\n jar, and at the hop to the issuer's /authorize adds the persona's\n `login_hint` and the gate; the emulator redirects to the app's callback, the\n app sets its session, and the cookies become the storage state. Every\n redirect hop must stay on a public `{port}` origin of the definition or the\n issuer, and a session cookie must be set after the issuer hop. Failures are\n reported as for Google, and the emulator's own log is in the copy at\n /var/lib/cofactor/logs/emulator.log. The helper requires the issuer to be\n https.\n\n A complete definition that passes `cofactor sandbox validate`:\n\n version: 1\n resources: { vcpus: 2, memory_mib: 4096 }\n setup: |\n apt-get update -qq\n apt-get install -y -qq curl unzip >/dev/null\n curl -fsSL https://bun.sh/install | bash >/dev/null\n ports:\n web: { port: 8400, access: public }\n params:\n web_url: { port: web }\n issuer_url: { port: emulation }\n session_secret: { secret: true }\n env:\n OIDC_ISSUER: ${params.issuer_url}\n OIDC_CLIENT_ID: sandbox\n OIDC_CLIENT_SECRET: sandbox\n OIDC_REDIRECT_URI: ${params.web_url}/auth/callback\n SESSION_SECRET: ${params.session_secret}\n services:\n app:\n run: bun server.ts\n health: { http: 8400, path: /health }\n reload_on_prepare: true\n personas:\n alice: { email: alice@example.test }\n bob: { email: bob@example.test }\n emulation:\n oidc: {}\n authentication:\n emulated:\n provider: oidc\n sign_in: ${params.web_url}/login\n lifecycle:\n build:\n - name: install\n run: bun install\n inputs: [bun.lock, package.json]\n prepare: []\n environments:\n default: { description: Stock data. }\n\n The sample app in the Cofactor repository, packages/demo-app-oidc, is a\n working example using the `openid-client` library.\n\nREFERENCES\n\n https://docs.runcofactor.com/reference/sandbox-definition#authentication\n https://docs.runcofactor.com/reference/sandbox-emulation#google-oauth-and-third-party-login\n cofactor sandbox guide v2 personas\n cofactor sandbox guide v2 emulation\n cofactor sandbox guide v2 ports\n";
|
|
@@ -315,10 +316,10 @@ var ports_default = "A sandbox can declare several ports, and each one gets its
|
|
|
315
316
|
var verify_default = "Two different checks, at two different costs.\n\n`cofactor sandbox validate [file]`\n\n Offline \u2014 no sandbox, no claim. Parses the YAML, runs the full\n schema, and cross-checks everything static analysis can catch: unknown\n actions or operations referenced by a seed, `${id.key}` references to a\n later step or an undeclared output key, an `extends` cycle, a duplicate\n step id across a chain, an unknown emulation plugin, port 80/443 declared\n while emulation is on, and more. Every issue is reported with a dotted\n path to exactly where it is, so a broken definition doesn't need a claim\n to find the problem:\n environments.default.seeds[0].input.workspace_id: references step \"acme\", which has not run yet (or does not exist)\n environments.free-plan.extends: unknown environment \"defualt\"\n ports.emulation: \"emulation\" is a reserved port name\n\n Run this before pushing any change to `cofactor.sandbox.yaml` \u2014 it's\n instant and catches most authoring mistakes before they cost a real\n claim.\n\n`cofactor sandbox verify-env --application A [--environment E]`\n\n Actually claims each requested environment of the application's current\n version (or every environment in the definition, if none is named) in turn:\n claim, wait for ready, report every seed step's output and timing, sign\n every persona in as a test's Login step would (when the definition\n declares `authentication`), then destroy. This is the only way to prove an\n environment's seed chain and persona logins actually run end to end \u2014\n `validate` can tell you a reference is well-formed, but only a real claim\n proves the referenced action produces the output at runtime, and that\n `authentication.run` works in the image (a command the image lacks, a\n wrong URL). Each persona prints `signed in`, or FAILED with the script's\n exit code and the end of its stderr; any failure fails the environment.\n\nWHEN A SEED FAILS\n\n Seeds run once per version (commit or push) and environment, while its\n ready snapshot is built. Any step's failure fails that snapshot \u2014 the\n builder is destroyed, so there's no partially-seeded sandbox left around\n to poke at \u2014 and the claims waiting on it fail with that error. The next\n claim builds the snapshot again, so a seed that fails every time keeps\n failing until a fix is committed or pushed. An infrastructure failure,\n by contrast, is retried by the build, within the claim's time limit\n (`cofactor sandbox claim --timeout`, 2 hours by default).\n\n To debug a failing seed:\n 1. Read the error \u2014 the claim (or `verify-env`) reports which step\n failed, its action, and the script's stderr (everything a script\n prints besides its final `{...}` result line goes to stderr, so\n this is where the real error message is).\n 2. Claim a sandbox **without** the failing environment (or on an\n environment that stops short of the failing step) so you have a\n live sandbox to work on.\n 3. Run the failing action by hand against that live sandbox with\n `cofactor sandbox run <id> <action> --input k=v ...`, using the\n same inputs the seed step would have resolved (substitute in the\n actual values for any `${id.key}` references by hand). This lets\n you iterate on the action's script without repeatedly re-claiming.\n 4. Once the action succeeds standalone, fix the seed definition or the\n action script and rerun `verify-env` to confirm the full chain\n works.\n";
|
|
316
317
|
|
|
317
318
|
// ../content/src/sandbox-v2-guide/cli.txt
|
|
318
|
-
var cli_default = "All commands are under `cofactor sandbox`, and every sandbox belongs to an\napplication (`--application <name|id>`). `--env` is the CLI's *global*\nflag for which Cofactor stage to talk to (`dev` / `prod`) \u2014 it has nothing\nto do with a sandbox's environment, which is always `--environment`.\n\ncofactor sandbox validate [file] [--upload]\n Offline schema + cross-check of a `cofactor.sandbox.yaml`. Defaults to\n the definition in the current directory if `file` is omitted. `--upload`\n checks a definition for pushed images (no `setup`);\n without it, one built from a GitHub checkout. See the `verify` topic for\n the issue format.\n\ncofactor sandbox connect --application A --repo owner/name [--dir D]\n Builds the application's sandboxes from a GitHub repository: the\n `cofactor.sandbox.yaml` under `--dir` (default: the repo root) at each\n commit. Its environments become the application's sandbox environments\n and its personas the application's personas. A new application name\n creates the application, with its sandbox `default` environment as its\n default environment; there is no base URL to give. It is created only\n after GitHub is connected (`cofactor github connect`), the repository is\n reachable and its definition is valid, so a refusal creates nothing.\n\ncofactor sandbox push --application A <image> [--definition F] [--tag T]... [--wait]\n For an application that gives Cofactor no repository access: pushes a\n built image (with its `cofactor.sandbox.yaml`) as the application's next\n version. A new application name creates the application, as `connect`\n does. See the `upload` topic.\n\ncofactor sandbox envs --application A [--json]\n The application's delivery (GitHub or pushed images), its current\n version, each environment's head (see `refresh`), recent pushes, sandbox environments, and what its definition\n offers: every environment (descriptions, seeds), ports, personas,\n actions and emulator operations (with their schemas). This is what an\n agent reads to decide which environment to claim and how to call each\n action once it has a sandbox.\n\ncofactor sandbox refresh --application A [--environment E] [--wait]\n Moves an environment's head to now. The head is what flow runs, the flow\n designer, scheduled runs and unpinned claims use: for GitHub, the ready\n snapshot of the default branch; for pushes, the newest push. It is never\n built per commit and never follows the default branch on its own, so it\n is normally behind it. It moves in two ways only: the background refresh\n (every 6 hours, for environments in use) and this command, which builds\n the default branch's tip as of this moment and advances the head when its\n snapshot is ready. A pushed application's head is already its newest push\n (\"already at head <push id>\"). Prints the old head, the new head and its\n build status; `--wait` blocks until the snapshot is ready or failed. A\n refresh while that snapshot is already building joins the build. Whoever\n can claim can refresh. `envs` shows each head and whether the default\n branch has moved past it, for information only.\n\ncofactor sandbox claim --application A [--environment E] [--at V] [--wait]\n Claims a sandbox: a fresh copy of the ready snapshot for the version and\n environment (built first \u2014 build, `prepare`, seeds \u2014 if it doesn't exist\n yet), given its own URLs and logins by `prepare`. A version is a commit\n SHA for a GitHub-delivered application, or a push id or tag for a pushed\n one. Without `--at`: the environment's head (see `refresh`); for GitHub,\n the default branch snapshot, else the default branch's tip if the head is\n stale; for pushes, the newest push. `--environment` defaults to `default`. `--wait`\n blocks until the sandbox is ready (or failed) instead of returning\n immediately with a claim id you'd otherwise have to poll with `show`.\n\ncofactor sandbox claims [--application A] [--json]\n Lists the sandboxes you claimed that are still claiming or ready: id,\n application, environment, status, when claimed and when the lease ends.\n Use it to find an id you lost, then `destroy` it.\n\ncofactor sandbox show <id>\n Current status, per-port URLs, persona logins (decrypted \u2014 this is where\n they're meant to be read from, never a PR comment), the snapshot's seed\n steps with the time each took, seed outputs, and the available actions\n and operations.\n\ncofactor sandbox run <id> <action|plugin.op> [--input k=v ...] [--json-input '{...}']\n Runs a definition action, an emulator operation (named `<plugin>.<op>`,\n e.g. `email.sent-emails`) or, when the definition declares `databases`,\n the built-in `database.fast-forward` (`--input database=<name>`; see the\n `data-fast-forward` topic) against a claimed sandbox, waits for it, and\n prints its output. `--input` is repeatable and coerced to the action/\n operation's declared type (string/number/integer/boolean/persona);\n `--json-input` passes the whole input as JSON, overriding `--input`.\n Invalid input prints the action or operation's input schema instead of\n running it, so you can see what's expected.\n\ncofactor sandbox exec <id> [--timeout S] [--json] -- <command...>\n Runs one shell command in a claimed sandbox, waits for it, and prints its\n output. One-shot: there is no interactive shell. It runs as the sandbox\n user in the definition's directory, with the environment actions get\n (the application's secrets included). Everything after `--` is the\n command, its words joined with single spaces (ssh semantics), so quote\n pipes and redirects yourself: `-- 'ls | wc -l'`.\n - Output is the last 4000 characters of stdout and of stderr, scrubbed of\n the sandbox's secrets and persona logins (best effort: an encoded value\n isn't caught). The CLI exits with the command's exit code (124 on a\n timeout). A failure that isn't the command's (the helper, or a sandbox\n that can't exec) prints `Error: ...` and exits 1. `--json` prints the\n whole run instead.\n - `--timeout S` is 1 to 1800 seconds (default 300). On a timeout the\n command's whole process group is killed. Background processes (`cmd &`)\n keep running after a normal exit; redirect their output\n (`cmd > log 2>&1 &`), because the exec's pipes close when it returns.\n If the CLI stops waiting (its deadline is the timeout plus 60 s), the\n command may still be running in the sandbox.\n - Permissions: a signed-in CLI session (`cofactor login`) isn't\n scope-checked, so every org member can exec, just as they can run\n actions. An API key needs the `sandbox:exec` scope explicitly:\n `sandbox:*` does not include it, full access (`*`) does. Create a key\n with `sandbox:exec` selected under Sandboxes in Settings -> API keys (or\n `POST /settings/api-keys` with `\"scopes\": [..., \"sandbox:exec\"]`).\n Existing `sandbox:*` keys don't gain it, and a key's scopes can't be\n edited, so mint a new key.\n - Commands and their scrubbed output are visible in the sandbox's run\n history to anyone with `sandbox:read`. Don't type credentials into a\n command; set them as secrets (see the `secrets` topic).\n - Every exec is audited in the organization's events: who ran it (and\n with which API key), the sandbox, the scrubbed command, and how it\n ended (exit code, or a failure). Never its output. An exec that cannot\n be audited does not run.\n - Platform: works on sandboxes built on a platform that has exec. An older\n sandbox answers \"This sandbox was built on an older platform; exec works\n once it is rebuilt on a newer one.\" Exec ships with the next sandbox\n platform update (it needs no bump of its own).\n\ncofactor sandbox sync <id> [--build] [--json]\ncofactor sandbox sync <id> <local-dir> --to <path> [--json]\n Makes a sandbox you claimed match your working tree, uncommitted and\n untracked changes included, straight over the sandbox's own sync port.\n It lists every file it writes (untracked ones marked) and deletes, then\n applies, records the synced tree id (`show` prints it) and extends the\n sandbox's lease (up to 8 h after the claim). Nothing restarts on its own:\n `exec <id> -- cofactor-sandbox restart <service>`. `--build` reruns the\n definition's changed build steps and restarts `after_build` services\n (GitHub and pushed applications alike). `--to` mirrors local build output into <path>. Needs the same\n `sandbox:exec` scope as exec. See the `local` topic.\n\ncofactor workflow run <flow> --sandbox <id>\n Runs a flow on a sandbox you claimed instead of a fresh copy, and never\n destroys it. The run targets the sandbox's own application and environment.\n An `--environment` of another application or sandbox environment is refused.\n The run records the version and synced tree (`cofactor workflow debug overview <run>`). An API key also needs\n `sandbox:read`.\n\ncofactor sandbox destroy <id>\n Tears the sandbox down. Always do this when a claimed sandbox is no\n longer needed \u2014 it isn't reclaimed automatically mid-session.\n\ncofactor sandbox secret set <NAME> --application A [--environment E] [--json]\n Stores a third-party secret (an API key) for the application, for every\n environment, or for just `--environment E` (an override). The value is\n read from stdin only and the CLI refuses a terminal:\n `printf %s \"$KEY\" | cofactor sandbox secret set NAME --application A`.\n One trailing newline is stripped. A name is an environment variable name\n (capital letters, digits and `_`, not starting with a digit; not PATH,\n HOME, USER, SHELL, NODE_OPTIONS, NODE_EXTRA_CA_CERTS, LD_* or COFACTOR_*);\n a value is 8 to 8192 bytes, with no NUL. Fast copies of an existing\n ready snapshot keep its old value until a new commit or image push\n rebuilds the snapshot. Legacy-prepare claims see the change on the next\n claim. Prints `Secret NAME set for all environments of A.` (or\n `... for environment \"E\" of A.`). See the `secrets` topic.\n\ncofactor sandbox secret list --application A [--json]\n Names and scopes (all environments, or one) with the time last set.\n Never values. With none set it prints `No secrets set for A.`\n\ncofactor sandbox secret rm <NAME> --application A [--environment E] [--json]\n Removes the secret at that scope: without `--environment`, the\n application-wide one; with it, that environment's override. Prints\n `Secret NAME removed for all environments of A.` (or\n `... for environment \"E\" of A.`). A secret that is not set fails with\n \"No secret with that name and scope.\". Fast copies of an existing ready\n snapshot keep the removed value until a new commit or image push rebuilds\n the snapshot. Legacy-prepare claims see the removal on the next claim.\n See the `secrets` topic.\n\ncofactor sandbox verify-env --application A [--environment E]\n Claim \u2192 wait \u2192 report seed outputs and timings \u2192 sign every persona in\n (when the definition declares `authentication`) \u2192 destroy, for each\n environment of the current version (or the one named). The end-to-end\n proof that an environment's seed chain and persona logins actually work,\n not just that they're well-formed. See the `verify` topic.\n\ncofactor sandbox preview --application A --pr N\n A pull request's preview of a GitHub-delivered application: status, URLs\n and the logins its PR comment deliberately leaves out.\n To run a test on it (a private copy at its applied head, never the shared preview):\n cofactor workflow run <workflowId> --preview N --application A\n and to list tests in a pull request as proof: `cofactor proofs guide`.\n\ncofactor sandbox guide [topic]\n Prints this guide (`guide v2 [topic]` is the same). Omit `topic` to\n print every section; name one (e.g. `upload`) to print just that topic.\n `guide v1 [section]` prints the guide for the earlier sandboxes (its\n `verify` section is `guide v1 verify`; plain `guide verify` is this\n guide's).\n\nTHE CORE LOOP\n\n validate \u2192 (connect | push) \u2192 envs \u2192 claim --wait \u2192 run (actions/\n operations, as many as the test needs) \u2192 destroy. Always validate a\n changed definition before pushing it \u2014 see the `verify` topic for why.\n";
|
|
319
|
+
var cli_default = "All commands are under `cofactor sandbox`, and every sandbox belongs to an\napplication (`--application <name|id>`). `--env` is the CLI's *global*\nflag for which Cofactor stage to talk to (`dev` / `prod`) \u2014 it has nothing\nto do with a sandbox's environment, which is always `--environment`.\n\ncofactor sandbox validate [file] [--upload]\n Offline schema + cross-check of a `cofactor.sandbox.yaml`. Defaults to\n the definition in the current directory if `file` is omitted. `--upload`\n checks a definition for pushed images (no `setup`);\n without it, one built from a GitHub checkout. See the `verify` topic for\n the issue format.\n\ncofactor sandbox connect --application A --repo owner/name [--dir D]\n Builds the application's sandboxes from a GitHub repository: the\n `cofactor.sandbox.yaml` under `--dir` (default: the repo root) at each\n commit. Its environments become the application's sandbox environments\n and its personas the application's personas. A new application name\n creates the application, with its sandbox `default` environment as its\n default environment; there is no base URL to give. It is created only\n after GitHub is connected (`cofactor github connect`), the repository is\n reachable and its definition is valid, so a refusal creates nothing.\n\ncofactor sandbox push --application A <image> [--definition F] [--tag T]... [--wait]\n For an application that gives Cofactor no repository access: pushes a\n built image (with its `cofactor.sandbox.yaml`) as the application's next\n version. A new application name creates the application, as `connect`\n does. See the `upload` topic.\n\ncofactor sandbox envs --application A [--json]\n The application's delivery (GitHub or pushed images), its current\n version, each environment's head (see `refresh`), recent pushes, sandbox environments, and what its definition\n offers: every environment (descriptions, seeds), ports, personas,\n actions and emulator operations (with their schemas). This is what an\n agent reads to decide which environment to claim and how to call each\n action once it has a sandbox.\n\ncofactor sandbox refresh --application A [--environment E] [--wait]\n Moves an environment's head to now. The head is what flow runs, the flow\n designer, scheduled runs and unpinned claims use: for GitHub, the ready\n snapshot of the default branch; for pushes, the newest push. It is never\n built per commit and never follows the default branch on its own, so it\n is normally behind it. It moves in two ways only: the background refresh\n (every 6 hours, for environments in use) and this command, which builds\n the default branch's tip as of this moment and advances the head when its\n snapshot is ready. A pushed application's head is already its newest push\n (\"already at head <push id>\"). Prints the old head, the new head and its\n build status; `--wait` blocks until the snapshot is ready or failed. A\n refresh while that snapshot is already building joins the build. Whoever\n can claim can refresh. `envs` shows each head and whether the default\n branch has moved past it, for information only.\n\ncofactor sandbox claim --application A [--environment E] [--at V] [--wait]\n Claims a sandbox: a fresh copy of the ready snapshot for the version and\n environment (built first \u2014 build, `prepare`, seeds \u2014 if it doesn't exist\n yet), given its own URLs and logins by `prepare`. A version is a commit\n SHA for a GitHub-delivered application, or a push id or tag for a pushed\n one. Without `--at`: the environment's head (see `refresh`); for GitHub,\n the default branch snapshot, else the default branch's tip if the head is\n stale; for pushes, the newest push. `--environment` defaults to `default`. `--wait`\n blocks until the sandbox is ready (or failed) instead of returning\n immediately with a claim id you'd otherwise have to poll with `show`.\n\ncofactor sandbox claims [--application A] [--json]\n Lists the sandboxes you claimed that are still claiming or ready: id,\n application, environment, status, when claimed and when the lease ends.\n Use it to find an id you lost, then `destroy` it.\n\ncofactor sandbox show <id>\n Current status, per-port URLs, persona logins (decrypted \u2014 this is where\n they're meant to be read from, never a PR comment), the snapshot's seed\n steps with the time each took, seed outputs, and the available actions\n and operations.\n\ncofactor sandbox run <id> <action|plugin.op> [--input k=v ...] [--json-input '{...}']\n Runs a definition action, an emulator operation (named `<plugin>.<op>`,\n e.g. `email.sent-emails`) or, when the definition declares `databases`,\n the built-in `database.fast-forward` (`--input database=<name>`; see the\n `data-fast-forward` topic) against a claimed sandbox, waits for it, and\n prints its output. `--input` is repeatable and coerced to the action/\n operation's declared type (string/number/integer/boolean/persona);\n `--json-input` passes the whole input as JSON, overriding `--input`.\n Invalid input prints the action or operation's input schema instead of\n running it, so you can see what's expected.\n\ncofactor sandbox exec <id> [--timeout S] [--json] -- <command...>\n Runs one shell command in a claimed sandbox, waits for it, and prints its\n output. One-shot: there is no interactive shell. It runs as the sandbox\n user in the definition's directory, with the environment actions get\n (the application's secrets included). Everything after `--` is the\n command, its words joined with single spaces (ssh semantics), so quote\n pipes and redirects yourself: `-- 'ls | wc -l'`.\n - Output is the last 4000 characters of stdout and of stderr, scrubbed of\n the sandbox's secrets and persona logins (best effort: an encoded value\n isn't caught). The CLI exits with the command's exit code (124 on a\n timeout). A failure that isn't the command's (the helper, or a sandbox\n that can't exec) prints `Error: ...` and exits 1. `--json` prints the\n whole run instead.\n - `--timeout S` is 1 to 1800 seconds (default 300). On a timeout the\n command's whole process group is killed. Background processes (`cmd &`)\n keep running after a normal exit; redirect their output\n (`cmd > log 2>&1 &`), because the exec's pipes close when it returns.\n If the CLI stops waiting (its deadline is the timeout plus 60 s), the\n command may still be running in the sandbox.\n - Permissions: a signed-in CLI session (`cofactor login`) isn't\n scope-checked, so every org member can exec, just as they can run\n actions. An API key needs the `sandbox:exec` scope explicitly:\n `sandbox:*` does not include it, full access (`*`) does. Create a key\n with `sandbox:exec` selected under Sandboxes in Settings -> API keys (or\n `POST /settings/api-keys` with `\"scopes\": [..., \"sandbox:exec\"]`).\n Existing `sandbox:*` keys don't gain it, and a key's scopes can't be\n edited, so mint a new key.\n - Commands and their scrubbed output are visible in the sandbox's run\n history to anyone with `sandbox:read`. Don't type credentials into a\n command; set them as secrets (see the `secrets` topic).\n - Every exec is audited in the organization's events: who ran it (and\n with which API key), the sandbox, the scrubbed command, and how it\n ended (exit code, or a failure). Never its output. An exec that cannot\n be audited does not run.\n - Platform: works on sandboxes built on a platform that has exec. An older\n sandbox answers \"This sandbox was built on an older platform; exec works\n once it is rebuilt on a newer one.\" Exec ships with the next sandbox\n platform update (it needs no bump of its own).\n\ncofactor sandbox sync <id> [--build] [--json]\ncofactor sandbox sync <id> <local-dir> --to <path> [--json]\n Makes a sandbox you claimed match your working tree, uncommitted and\n untracked changes included, straight over the sandbox's own sync port.\n It lists every file it writes (untracked ones marked) and deletes, then\n applies, records the synced tree id (`show` prints it) and extends the\n sandbox's lease (up to 8 h after the claim). Nothing restarts on its own:\n `exec <id> -- cofactor-sandbox restart <service>`. `--build` reruns the\n definition's changed build steps and restarts `after_build` services\n (GitHub and pushed applications alike). `--to` mirrors local build output into <path>. Needs the same\n `sandbox:exec` scope as exec. See the `local` topic.\n\ncofactor workflow run <flow> --sandbox <id>\n Runs a flow on a sandbox you claimed instead of a fresh copy, and never\n destroys it. The run targets the sandbox's own application and environment.\n An `--environment` of another application or sandbox environment is refused.\n The run records the version and synced tree (`cofactor workflow debug overview <run>`). An API key also needs\n `sandbox:read`.\n\ncofactor sandbox destroy <id>\n Tears the sandbox down. Always do this when a claimed sandbox is no\n longer needed \u2014 it isn't reclaimed automatically mid-session.\n\ncofactor sandbox secret set <NAME> --application A [--environment E] [--json]\n Stores a third-party secret (an API key) for the application, for every\n environment, or for just `--environment E` (an override). The value is\n read from stdin only and the CLI refuses a terminal:\n `printf %s \"$KEY\" | cofactor sandbox secret set NAME --application A`.\n One trailing newline is stripped. A name is an environment variable name\n (capital letters, digits and `_`, not starting with a digit; not PATH,\n HOME, USER, SHELL, NODE_OPTIONS, NODE_EXTRA_CA_CERTS, LD_* or COFACTOR_*);\n a value is 8 to 8192 bytes, with no NUL. Fast copies of an existing\n ready snapshot keep its old value until a new commit or image push\n rebuilds the snapshot. Legacy-prepare claims see the change on the next\n claim. Prints `Secret NAME set for all environments of A.` (or\n `... for environment \"E\" of A.`). See the `secrets` topic.\n\ncofactor sandbox secret list --application A [--json]\n Names and scopes (all environments, or one) with the time last set.\n Never values. With none set it prints `No secrets set for A.`\n\ncofactor sandbox secret rm <NAME> --application A [--environment E] [--json]\n Removes the secret at that scope: without `--environment`, the\n application-wide one; with it, that environment's override. Prints\n `Secret NAME removed for all environments of A.` (or\n `... for environment \"E\" of A.`). A secret that is not set fails with\n \"No secret with that name and scope.\". Fast copies of an existing ready\n snapshot keep the removed value until a new commit or image push rebuilds\n the snapshot. Legacy-prepare claims see the removal on the next claim.\n See the `secrets` topic.\n\ncofactor sandbox verify-env --application A [--environment E]\n Claim \u2192 wait \u2192 report seed outputs and timings \u2192 sign every persona in\n (when the definition declares `authentication`) \u2192 destroy, for each\n environment of the current version (or the one named). The end-to-end\n proof that an environment's seed chain and persona logins actually work,\n not just that they're well-formed. See the `verify` topic.\n\ncofactor sandbox preview --application A --pr N\n A pull request's preview of a GitHub-delivered application: status, URLs,\n the logins GitHub deliberately never sees, and the URL of its Cofactor page\n (the same details plus build output on failure, for signed-in org members).\n The PR's GitHub deployment links that page as its \"View details\".\n To run a test on it (a private copy at its applied head, never the shared preview):\n cofactor workflow run <workflowId> --preview N --application A\n and to list tests in a pull request as proof: `cofactor proofs guide`.\n\ncofactor sandbox guide [topic]\n Prints this guide (`guide v2 [topic]` is the same). Omit `topic` to\n print every section; name one (e.g. `upload`) to print just that topic.\n `guide v1 [section]` prints the guide for the earlier sandboxes (its\n `verify` section is `guide v1 verify`; plain `guide verify` is this\n guide's).\n\nTHE CORE LOOP\n\n validate \u2192 (connect | push) \u2192 envs \u2192 claim --wait \u2192 run (actions/\n operations, as many as the test needs) \u2192 destroy. Always validate a\n changed definition before pushing it \u2014 see the `verify` topic for why.\n";
|
|
319
320
|
|
|
320
321
|
// ../content/src/sandbox-v2-guide/upload.txt
|
|
321
|
-
var upload_default = 'PUSHED IMAGES: SANDBOXES WITHOUT REPOSITORY ACCESS\n\nAn application whose team gives Cofactor no access to its repository\ndelivers its sandboxes as built images: build your app\'s image as usual,\nthen push it with its `cofactor.sandbox.yaml` whenever you have a new\nbuild (nightly, per release, per merge \u2014 your cadence, your CI or your\nlaptop). Each push is a version of the application. A tag names the newest\nready push that carries it: pushing `--tag nightly` again moves the tag to\nthe new push, and older pushes stay claimable by their id.\n\nTag your main builds `main` (or `nightly`). These two tags are reserved: once\nthe application has a push carrying either, its head (what the flow designer,\nscheduled runs and claims without `--at` use) is the newest push tagged `main`\nor `nightly`, and only those runs move the visual-review baseline. Pull\nrequest and branch pushes are compared against them and change nothing. Until\nthen the newest push is the head. Claim any other push with\n`--at <push id or tag>`.\n\nUse tags that move (`nightly`, `release`, a branch name), not one per push\nsuch as a date. Cofactor keeps the layers of each application\'s newest push,\nits head, and its 10 newest tagged pushes. Older pushes stay claimable, but when the\nsandbox platform is updated only kept pushes move to it on their own; an\nolder one says to push again.\n\n docker buildx build --platform linux/amd64 -t tracker:sandbox --load .\n cofactor sandbox validate --upload cofactor.sandbox.yaml\n cofactor sandbox push --application "Project Tracker" tracker:sandbox \\\n --definition cofactor.sandbox.yaml --tag nightly --wait\n cofactor sandbox envs --application "Project Tracker"\n cofactor sandbox claim --application "Project Tracker" --environment default --wait\n\n`push` adds Cofactor\'s layers to your image (its sandbox helper as the\nentrypoint, plus the object storage server when the definition declares\n`object_storage`) and a small layer with your definition, reads the image\nout of your local Docker (`docker save`), and uploads only the layers\nCofactor doesn\'t already have for your organization (see FAST RE-PUSHES).\nCofactor boots it once until every service is healthy (`--wait` waits for\nthat). Then each environment in the definition\nbecomes a sandbox environment of the application, and each persona one of\nits personas. The first claim of an environment builds its ready snapshot\n(prepare + seeds, about a minute); later claims copy it in seconds.\n\nWHAT THE IMAGE MUST BE\n\n - linux/amd64. On an Apple Silicon Mac, build with\n `--platform linux/amd64`; `push` refuses any other platform.\n - glibc-based with bash (Debian, Ubuntu, `python:*-slim`, `node:*`\n non-alpine\u2026). Alpine/musl and distroless images do not work: the\n helper is a glibc binary and every command runs through `bash -lc`.\n If only your final stage is Alpine, keep the build stages and change\n that one `FROM` to its Debian equivalent (`node:24-alpine` \u2192\n `node:24-bookworm-slim`, `python:3.12-alpine` \u2192 `python:3.12-slim`),\n with `apt-get install` in place of `apk add`. Native dependencies\n compiled on Alpine don\'t run on glibc: install them in a glibc stage\n rather than copying `node_modules` or wheels out of an Alpine one.\n - A WORKDIR other than `/`. Services, prepare commands and actions run\n from it, and the helper gives it to the sandbox user (below).\n - Built already. Nothing builds at push or claim: everything the app\n needs (dependencies, compiled assets, a database server if it uses one)\n is in the image. Optional `lifecycle.build` steps run only on\n `sync --build` (the `local` topic).\n\nFAST RE-PUSHES\n\n A push uploads only the layers that changed since an earlier push (in\n the last 30 days); `push` says how much it sent and how much Cofactor\n already had. These keep a re-push small:\n\n - Order the Dockerfile so what changes most comes last: system packages,\n then the dependency manifest and lockfile and the install step, then\n your source. A code change then rebuilds, and uploads, only the last\n layers:\n FROM python:3.12-slim\n RUN apt-get update && apt-get install -y --no-install-recommends curl jq\n WORKDIR /app\n COPY requirements.txt .\n RUN pip install -r requirements.txt\n COPY . .\n - In CI, keep the build cache between runs. A runner that starts with an\n empty cache rebuilds every step, and every rebuilt layer is new to\n Cofactor even when nothing in it changed. With GitHub Actions, build\n with Docker\'s official actions, which pass the Actions cache to\n buildx for you:\n - uses: docker/setup-buildx-action@v4\n - uses: docker/build-push-action@v7\n with:\n context: .\n platforms: linux/amd64\n load: true\n tags: app:sandbox\n cache-from: type=gha\n cache-to: type=gha,mode=max\n (or `type=registry` with a cache image of your own). Docker\'s own\n actions are pinned by major version here, like `actions/checkout`;\n pin them to a commit if your policy requires it. A raw\n `docker buildx build ... --cache-from type=gha` in a `run:` step\n can\'t see the Actions cache token and skips the cache without a\n warning. If you must use one, add the community action\n `crazy-max/ghaction-github-runtime` before it, pinned to a full commit\n (`@04d248b84655b509d8c44dc1d6f990c879747487`, v4.0.0): it exposes the\n Actions runtime token to every later step of the job.\n - For compiled stacks (.NET, Java, Rust), build outside Docker and keep\n the Dockerfile thin. `RUN --mount=type=cache` (NuGet, Maven, cargo,\n incremental `obj/` or `target/` directories) is not exported by\n `--cache-to type=gha`, so inside a Dockerfile those caches are empty\n on every CI run and the whole compile reruns. Compile on the runner\n with `actions/cache` for the compiler\'s incremental state, and have\n the Dockerfile only `COPY` the published output onto a base image\n holding what rarely changes (database server, runtimes, seed data;\n rebuild it only when its inputs change). Three details decide whether\n the re-push is actually small:\n - Incremental compilers (MSBuild, make, cargo) compare file mtimes.\n A fresh `git clone` stamps every file "now" and invalidates the\n cached state. Before compiling, set tracked files to an old date\n (`git ls-files -z | xargs -0 touch -h -d \'2000-01-01\'`), then\n touch the files changed since the commit you last built. Keep its\n SHA in the cache entry, check out with `fetch-depth: 0` so it is\n present, and skip this on a cache miss (that build is full anyway):\n `git diff -z --name-only --diff-filter=d <last-sha> HEAD | xargs -0 -r touch -h`.\n - Layers are digested by content including mtimes, and the parent\n directories a COPY creates can get the build time, so identical\n files can yield a new layer digest on every build. Stage the\n output under its final path (`staged/opt/app/...`), pin every\n mtime in it including the directories\n (`find staged -exec touch -h -d \'2000-01-01\' {} +`), and use\n `COPY --link staged/ /` (BuildKit).\n - Split third-party files from your own into separate COPYs so a\n code change rewrites only the small one.\n\n CI authenticates with an API key (`COFACTOR_API_TOKEN`). Give it only\n what the job does: `sandbox:read`, `sandbox:publish` and `property:read`\n to push (plus `property:create` if CI\'s push may be the first, which\n creates the application); add `sandbox:create` and `sandbox:delete` to claim and destroy,\n `sandbox:run` to run actions, and `sandbox:exec` to run shell commands\n (not included in `sandbox:*`). Create it from the CLI (the secret prints\n once): `cofactor api-key create --name "CI image push" --scope sandbox:read\n --scope sandbox:publish --scope property:read`.\n\n Push main builds with `--tag main` (or `nightly`). For pull requests, push\n each with its own tag and claim it by that tag:\n cofactor sandbox push --application "Project Tracker" app:sandbox --tag pr-123 --wait\n cofactor sandbox claim --application "Project Tracker" --environment default --at pr-123 --wait\n A pull request\'s push shares every layer below the source with main\'s.\n\nNIGHTLY PUSH FROM GITHUB ACTIONS\n\n A complete `.github/workflows/cofactor-nightly.yml` for the example\n application "Acme Tracker": it builds the image every night (and on\n demand), pushes it tagged `nightly`, then runs your suite on that push.\n Replace the application and suite names with yours.\n\n Before the first run, create the CI key (see above) and store it as the\n repository secret COFACTOR_API_TOKEN. For the last step add `--scope\n test-run:run --scope test-run:read`; without that step the push scopes\n are enough.\n\n name: cofactor-nightly\n on:\n schedule:\n - cron: "0 3 * * *"\n workflow_dispatch:\n concurrency:\n group: cofactor-nightly\n cancel-in-progress: false\n permissions:\n contents: read\n jobs:\n nightly:\n runs-on: ubuntu-latest\n timeout-minutes: 180\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npm install -g @runcofactor/cli\n - uses: docker/setup-buildx-action@v4\n - name: Build the image\n uses: docker/build-push-action@v7\n with:\n context: .\n platforms: linux/amd64\n load: true\n tags: acme-tracker:sandbox\n cache-from: type=gha\n cache-to: type=gha,mode=max\n - name: Check the definition\n run: cofactor sandbox validate --upload cofactor.sandbox.yaml\n - name: Push the image\n env:\n COFACTOR_API_TOKEN: ${{ secrets.COFACTOR_API_TOKEN }}\n run: |\n cofactor sandbox push --application "Acme Tracker" acme-tracker:sandbox \\\n --definition cofactor.sandbox.yaml --tag nightly --wait\n - name: Run the suite on the new push\n env:\n COFACTOR_API_TOKEN: ${{ secrets.COFACTOR_API_TOKEN }}\n run: cofactor test-suite run --suite "Acme Tracker nightly"\n\n The CLI reads `COFACTOR_API_TOKEN` from the environment (no login step);\n only the steps that call Cofactor get it, and `validate` needs none.\n The build step\'s cache settings are the ones under FAST RE-PUSHES.\n `--wait` follows the import for up to 50 minutes and fails the job if\n Cofactor cannot boot the image. A push still importing after 50\n minutes is not a failure: the job moves on and the suite runs on the\n previous head. The suite runs on the application\'s head: the newest push\n tagged `main` or `nightly`. Start the workflow by hand with "Run\n workflow" (the `workflow_dispatch` trigger) to test a change to this\n file. `timeout-minutes` covers a build, the longest import wait and the\n suite; raise it if your suite runs longer.\n\n The last step is optional: a suite can also run on a Cofactor schedule.\n\nMAKING YOUR IMAGE SYNCABLE\n\n `cofactor sandbox sync` writes your working tree into a claimed sandbox\n (see `guide v2 local`). For a pushed image it syncs the directory holding\n cofactor.sandbox.yaml into the image\'s WORKDIR, so the image must keep\n that directory\'s files at WORKDIR in the same layout. The rule: a\n syncable image has your definition file at its WORKDIR, as `COPY . .`\n gives you.\n\n WORKDIR /app\n COPY . .\n RUN npm ci && npm run build\n\n A multi-stage build that copies only some directories works too, as\n long as each lands at the same relative path under WORKDIR and the\n definition file is copied in:\n WORKDIR /app\n COPY --from=build /src/api /app/api\n COPY cofactor.sandbox.yaml /app/cofactor.sandbox.yaml\n\n Files the image doesn\'t ship (sources it compiles away, docs) are fine:\n `sync` writes them as new files and nothing runs them.\n\n Compiled output isn\'t rebuilt in the sandbox (nothing builds at push or claim; `sync --build` reruns a declared `lifecycle.build`):\n build it locally, then sync the result to where the image serves it:\n npm run build\n cofactor sandbox sync <id> dist --to public\n\n `push` checks the rule on the local image and says one of:\n Sync: syncable.\n Sync: syncable (the image is older than your working tree, which is fine).\n Sync: not syncable: cofactor.sandbox.yaml isn\'t at /app/cofactor.sandbox.yaml (...)\n Sync: not checked (...) (Docker, or git, couldn\'t answer)\n It never stops a push. It reads the definition at your WORKDIR, not the\n copy `push` adds for Cofactor itself.\n\nHOW YOUR IMAGE RUNS IN A SANDBOX\n\n - Its ENTRYPOINT and CMD are NOT used. Start each process yourself with a\n `services.<name>.run` command (often your entrypoint script).\n - Its ENV and WORKDIR ARE applied, as `docker run` would, its PATH\n included: commands run through a `bash -lc` login shell, and the image\'s\n own PATH is put back in front of whatever the login profile sets.\n - Leave the image\'s USER as root (or unset). The sandbox starts as root\n and drops to uid 1000 itself; an image that ended on another `USER`\n has failed to boot with a permission error on `/etc/cofactor`.\n - Everything \u2014 services, prepare commands, actions \u2014 runs as an\n unprivileged user with uid 1000 (the sandbox adds that account if your\n image has none). The WORKDIR tree is made theirs; anything the app\n writes must be under the WORKDIR or `/tmp`, not elsewhere in a root-\n owned path.\n - One machine, no Docker inside: a database is a process you run as a\n service (Postgres installed in the image, or SQLite), not a container.\n For S3 or GCS, declare `object_storage` and the sandbox runs the\n server for you (see the `object-storage` topic).\n - Each public port gets its own https URL, unique per sandbox. Its\n origin is a param \u2014 `params: { web_url: { port: web } }` \u2014 that you\n pass to the app through `env` (e.g. `PUBLIC_URL: ${params.web_url}`).\n\nTHE DEFINITION FOR PUSHED IMAGES\n\n The same `cofactor.sandbox.yaml` as for a repository (see the\n `definition` topic), minus installing the toolchain: no `setup`. `lifecycle.build`\n and `after_build` are optional and run only on `sync --build`. A minimal one for the example app,\n built as one image that serves its UI and API on port 8080:\n\n version: 1\n resources: { vcpus: 2, memory_mib: 4096 }\n ports:\n web: { port: 8080, access: public }\n params:\n web_url: { port: web }\n env:\n PUBLIC_URL: ${params.web_url}\n services:\n app:\n run: exec ./start.sh\n health: { http: 8080, path: /health }\n reload_on_prepare: true\n personas:\n owner: { email: owner@example.test }\n lifecycle:\n prepare:\n - bash sandbox/personas.sh\n actions:\n create-workspace:\n description: A new workspace, created by the given persona, who becomes its owner.\n run: sandbox/actions/create-workspace.sh\n input:\n name: { type: string, required: true }\n as: { type: persona, required: true }\n output:\n workspace_id: { type: string }\n environments:\n default:\n description: Workspace "Acme", owned by the owner persona.\n seeds:\n - id: acme\n action: create-workspace\n input: { name: Acme, as: owner }\n\n From here, add what your tests need, in the order "WHAT TO BUILD FIRST"\n in the `seeds-and-actions` topic gives: a persona per role, an\n environment per plan or flag combination, and actions for the core\n objects.\n\n Keep the file anywhere in your repo: `validate --upload <file>` and\n `push --definition <file>` read it from where you point them, and the\n push puts it in the image. Paths inside it (`run:` of actions, prepare\n commands) are relative to the WORKDIR, so ship your sandbox scripts in\n the image (e.g. `COPY sandbox/ sandbox/`).\n\n Services start when the image is first booted, BEFORE any sandbox exists,\n with params at placeholder values. A service that needs the sandbox\'s own\n values (its URL, a `{secret: true}` param) or reads a secret sets\n `reload_on_prepare: true`: it is started (and restarted) by each\n sandbox\'s prepare instead. In doubt, set it on your app\'s service.\n\n `health` passes when the http path answers below 400 (or the command\n exits 0) within 5 minutes of the service starting.\n\nPERSONAS: MAKING THE LOGINS WORK\n\n Cofactor generates a fresh password for every persona when a snapshot is\n built; every sandbox copied from it has the same ones. Your\n `lifecycle.prepare` commands (which also run on each copy) receive them as `COFACTOR_PERSONAS`, a\n JSON map `{ "<persona>": { "email": "...", "password": "..." } }`, and\n must make each login work in your app: create or update that user with\n that password (through your app\'s API, its CLI, or its database). A\n persona whose login doesn\'t work breaks every test that signs in as it.\n A persona\'s `email` is yours to choose: pick an account your app already\n has (its seed data) and set its password, or have prepare create it.\n Cofactor\'s test runs then sign in with these logins automatically, as\n the application\'s persona of the same name.\n\n Prepare runs while your app is up: the `reload_on_prepare` services are\n running and healthy (restarted first when their environment changed),\n the others have run since the image was booted. So write the logins\n somewhere the running app reads on every sign-in (its database, or its\n own API). If your app loads its accounts once at startup (a file or\n config read into memory), change it to read them per request, or the new\n passwords won\'t take. For an app whose logins are fixtures, a common fix\n is a small JSON file the prepare script writes from `COFACTOR_PERSONAS`\n and the app checks on each sign-in before its fixtures.\n\nACTIONS AND SEEDS\n\n An action is a script in the image with typed input and output (the\n `seeds-and-actions` topic covers designing them). Inputs arrive as\n `INPUT_<NAME>` env vars (a `persona` input carries the persona\'s name,\n e.g. `owner`), alongside the definition\'s `env:`, every param as\n `COFACTOR_PARAM_<NAME>`, and `COFACTOR_PERSONAS`. The script prints its\n result as a JSON object on the LAST stdout line that starts with `{`\n (send everything else to stderr). It runs inside the sandbox, so it\n reaches your app on localhost. An environment\'s `seeds` call actions in\n order, once per version, before the snapshot every claim copies;\n `${<step id>.<output>}` passes one step\'s output to a later step. Run\n one against a live sandbox with `cofactor sandbox run <id> <action>\n --input name=... --input as=owner`.\n\n An action that signs in as a persona and creates data through the app\'s\n own API (here a cookie-session JSON API on port 8080; the port, paths and\n JSON fields are the example app\'s, use your app\'s). It needs jq and\n curl, which slim base images lack: add them to your Dockerfile\n (`apt-get install -y --no-install-recommends jq curl`).\n\n #!/usr/bin/env bash\n # sandbox/actions/create-workspace.sh\n set -euo pipefail\n login=$(jq -c --arg p "$INPUT_AS" \'.[$p] | {email, password}\' <<<"$COFACTOR_PERSONAS")\n jar=$(mktemp)\n curl -fsS -c "$jar" -H \'content-type: application/json\' \\\n -d "$login" http://127.0.0.1:8080/api/auth/login >&2\n id=$(curl -fsS -b "$jar" -H \'content-type: application/json\' \\\n -d "$(jq -nc --arg n "$INPUT_NAME" \'{name: $n}\')" \\\n http://127.0.0.1:8080/api/workspaces | jq -er \'.id\')\n jq -nc --arg id "$id" \'{workspace_id: $id}\'\n\n\nTROUBLESHOOTING\n\n - `push` says the image is not linux/amd64, or its WORKDIR can\'t be\n used: rebuild as it says.\n - The push fails with "Cofactor could not start this image": a service\n exited or never turned healthy. The message ends with that service\'s\n log. To reproduce, open a shell in the image \u2014\n docker run --rm -it --entrypoint bash <image>\n \u2014 and run that service\'s `run` command from the WORKDIR. In the\n sandbox it runs as uid 1000 with the WORKDIR made theirs; plain\n `docker run --user 1000` leaves the WORKDIR root-owned, so a write\n failure there is not the sandbox\'s.\n - A claim fails in prepare or a seed: the error carries the command\'s\n output. Fix the script, rebuild the image and push again.\n - To try prepare and actions before pushing, run the app as usual and\n call your scripts in it with the variables they read (a sandbox also\n sets the definition\'s `env:` and runs them as uid 1000):\n docker run -d --name try -p 8080:8080 <image>\n docker exec -e COFACTOR_PERSONAS=\'{"owner":{"email":"owner@example.test","password":"pw-1"}}\' try bash sandbox/personas.sh\n docker exec -e COFACTOR_PERSONAS=\'\u2026\' -e INPUT_NAME=Acme -e INPUT_AS=owner try bash sandbox/actions/create-workspace.sh\n (`docker exec` starts in the image\'s WORKDIR.)\n - `cofactor sandbox logs` and `diagnostics` are for the older sandboxes;\n for these, `cofactor sandbox show <id>` has the status and any error.\n - "push the image again": Cofactor\'s sandbox helper changed since that\n push; a fresh `cofactor sandbox push` picks up the current one.\n - A layer larger than 5 GB is refused; split or slim it.\n - Every push uploads the whole image: the build cache is missing (see\n FAST RE-PUSHES), or the Dockerfile copies the source before\n installing dependencies.\n';
|
|
322
|
+
var upload_default = 'PUSHED IMAGES: SANDBOXES WITHOUT REPOSITORY ACCESS\n\nAn application whose team gives Cofactor no access to its repository\ndelivers its sandboxes as built images: build your app\'s image as usual,\nthen push it with its `cofactor.sandbox.yaml` whenever you have a new\nbuild (nightly, per release, per merge \u2014 your cadence, your CI or your\nlaptop). Each push is a version of the application. A tag names the newest\nready push that carries it: pushing `--tag nightly` again moves the tag to\nthe new push, and older pushes stay claimable by their id.\n\nTag your main builds `main` (or `nightly`). These two tags are reserved: once\nthe application has a push carrying either, its head (what the flow designer,\nscheduled runs and claims without `--at` use) is the newest push tagged `main`\nor `nightly`, and only those runs move the visual-review baseline. Pull\nrequest and branch pushes are compared against them and change nothing. Until\nthen the newest push is the head. Claim any other push with\n`--at <push id or tag>`.\n\nUse tags that move (`nightly`, `release`, a branch name), not one per push\nsuch as a date. Cofactor keeps the layers of each application\'s newest push,\nits head, and its 10 newest tagged pushes. Older pushes stay claimable, but when the\nsandbox platform is updated only kept pushes move to it on their own; an\nolder one says to push again.\n\n docker buildx build --platform linux/amd64 -t tracker:sandbox --load .\n cofactor sandbox validate --upload cofactor.sandbox.yaml\n cofactor sandbox push --application "Project Tracker" tracker:sandbox \\\n --definition cofactor.sandbox.yaml --tag nightly --wait\n cofactor sandbox envs --application "Project Tracker"\n cofactor sandbox claim --application "Project Tracker" --environment default --wait\n\n`push` adds Cofactor\'s layers to your image (its sandbox helper as the\nentrypoint, plus the object storage server when the definition declares\n`object_storage`) and a small layer with your definition, reads the image\nout of your local Docker (`docker save`), and uploads only the layers\nCofactor doesn\'t already have for your organization (see FAST RE-PUSHES).\nCofactor boots it once until every service is healthy (`--wait` waits for\nthat). Then each environment in the definition\nbecomes a sandbox environment of the application, and each persona one of\nits personas. The first claim of an environment builds its ready snapshot\n(prepare + seeds, about a minute); later claims copy it in seconds.\n\nWHAT THE IMAGE MUST BE\n\n - linux/amd64. On an Apple Silicon Mac, build with\n `--platform linux/amd64`; `push` refuses any other platform.\n - glibc-based with bash (Debian, Ubuntu, `python:*-slim`, `node:*`\n non-alpine\u2026). Alpine/musl and distroless images do not work: the\n helper is a glibc binary and every command runs through `bash -lc`.\n If only your final stage is Alpine, keep the build stages and change\n that one `FROM` to its Debian equivalent (`node:24-alpine` \u2192\n `node:24-bookworm-slim`, `python:3.12-alpine` \u2192 `python:3.12-slim`),\n with `apt-get install` in place of `apk add`. Native dependencies\n compiled on Alpine don\'t run on glibc: install them in a glibc stage\n rather than copying `node_modules` or wheels out of an Alpine one.\n - A WORKDIR other than `/`. Services, prepare commands and actions run\n from it, and the helper gives it to the sandbox user (below).\n - Built already. Nothing builds at push or claim: everything the app\n needs (dependencies, compiled assets, a database server if it uses one)\n is in the image. Optional `lifecycle.build` steps run only on\n `sync --build` (the `local` topic), which needs `git` installed in the\n image: without it the build fails with `Executable not found in $PATH: "git"`.\n\nFAST RE-PUSHES\n\n A push uploads only the layers that changed since an earlier push (in\n the last 30 days); `push` says how much it sent and how much Cofactor\n already had. These keep a re-push small:\n\n - Order the Dockerfile so what changes most comes last: system packages,\n then the dependency manifest and lockfile and the install step, then\n your source. A code change then rebuilds, and uploads, only the last\n layers:\n FROM python:3.12-slim\n RUN apt-get update && apt-get install -y --no-install-recommends curl jq\n WORKDIR /app\n COPY requirements.txt .\n RUN pip install -r requirements.txt\n COPY . .\n - In CI, keep the build cache between runs. A runner that starts with an\n empty cache rebuilds every step, and every rebuilt layer is new to\n Cofactor even when nothing in it changed. With GitHub Actions, build\n with Docker\'s official actions, which pass the Actions cache to\n buildx for you:\n - uses: docker/setup-buildx-action@v4\n - uses: docker/build-push-action@v7\n with:\n context: .\n platforms: linux/amd64\n load: true\n tags: app:sandbox\n cache-from: type=gha\n cache-to: type=gha,mode=max\n (or `type=registry` with a cache image of your own). Docker\'s own\n actions are pinned by major version here, like `actions/checkout`;\n pin them to a commit if your policy requires it. A raw\n `docker buildx build ... --cache-from type=gha` in a `run:` step\n can\'t see the Actions cache token and skips the cache without a\n warning. If you must use one, add the community action\n `crazy-max/ghaction-github-runtime` before it, pinned to a full commit\n (`@04d248b84655b509d8c44dc1d6f990c879747487`, v4.0.0): it exposes the\n Actions runtime token to every later step of the job.\n - For compiled stacks (.NET, Java, Rust), build outside Docker and keep\n the Dockerfile thin. `RUN --mount=type=cache` (NuGet, Maven, cargo,\n incremental `obj/` or `target/` directories) is not exported by\n `--cache-to type=gha`, so inside a Dockerfile those caches are empty\n on every CI run and the whole compile reruns. Compile on the runner\n with `actions/cache` for the compiler\'s incremental state, and have\n the Dockerfile only `COPY` the published output onto a base image\n holding what rarely changes (database server, runtimes, seed data;\n rebuild it only when its inputs change). Three details decide whether\n the re-push is actually small:\n - Incremental compilers (MSBuild, make, cargo) compare file mtimes.\n A fresh `git clone` stamps every file "now" and invalidates the\n cached state. Before compiling, set tracked files to an old date\n (`git ls-files -z | xargs -0 touch -h -d \'2000-01-01\'`), then\n touch the files changed since the commit you last built. Keep its\n SHA in the cache entry, check out with `fetch-depth: 0` so it is\n present, and skip this on a cache miss (that build is full anyway):\n `git diff -z --name-only --diff-filter=d <last-sha> HEAD | xargs -0 -r touch -h`.\n - Layers are digested by content including mtimes, and the parent\n directories a COPY creates can get the build time, so identical\n files can yield a new layer digest on every build. Stage the\n output under its final path (`staged/opt/app/...`), pin every\n mtime in it including the directories\n (`find staged -exec touch -h -d \'2000-01-01\' {} +`), and use\n `COPY --link staged/ /` (BuildKit).\n - Split third-party files from your own into separate COPYs so a\n code change rewrites only the small one.\n\n CI authenticates with an API key (`COFACTOR_API_TOKEN`). Give it only\n what the job does: `sandbox:read`, `sandbox:publish` and `property:read`\n to push (plus `property:create` if CI\'s push may be the first, which\n creates the application); add `sandbox:create` and `sandbox:delete` to claim and destroy,\n `sandbox:run` to run actions, and `sandbox:exec` to run shell commands\n (not included in `sandbox:*`). Create it from the CLI (the secret prints\n once): `cofactor api-key create --name "CI image push" --scope sandbox:read\n --scope sandbox:publish --scope property:read`.\n\n Push main builds with `--tag main` (or `nightly`). For pull requests, push\n each with its own tag and claim it by that tag:\n cofactor sandbox push --application "Project Tracker" app:sandbox --tag pr-123 --wait\n cofactor sandbox claim --application "Project Tracker" --environment default --at pr-123 --wait\n A pull request\'s push shares every layer below the source with main\'s.\n\nNIGHTLY PUSH FROM GITHUB ACTIONS\n\n A complete `.github/workflows/cofactor-nightly.yml` for the example\n application "Acme Tracker": it builds the image every night (and on\n demand), pushes it tagged `nightly`, then runs your suite on that push.\n Replace the application and suite names with yours.\n\n Before the first run, create the CI key (see above) and store it as the\n repository secret COFACTOR_API_TOKEN. For the last step add `--scope\n test-run:run --scope test-run:read`; without that step the push scopes\n are enough.\n\n name: cofactor-nightly\n on:\n schedule:\n - cron: "0 3 * * *"\n workflow_dispatch:\n concurrency:\n group: cofactor-nightly\n cancel-in-progress: false\n permissions:\n contents: read\n jobs:\n nightly:\n runs-on: ubuntu-latest\n timeout-minutes: 180\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npm install -g @runcofactor/cli\n - uses: docker/setup-buildx-action@v4\n - name: Build the image\n uses: docker/build-push-action@v7\n with:\n context: .\n platforms: linux/amd64\n load: true\n tags: acme-tracker:sandbox\n cache-from: type=gha\n cache-to: type=gha,mode=max\n - name: Check the definition\n run: cofactor sandbox validate --upload cofactor.sandbox.yaml\n - name: Push the image\n env:\n COFACTOR_API_TOKEN: ${{ secrets.COFACTOR_API_TOKEN }}\n run: |\n cofactor sandbox push --application "Acme Tracker" acme-tracker:sandbox \\\n --definition cofactor.sandbox.yaml --tag nightly --wait\n - name: Run the suite on the new push\n env:\n COFACTOR_API_TOKEN: ${{ secrets.COFACTOR_API_TOKEN }}\n run: cofactor test-suite run --suite "Acme Tracker nightly"\n\n The CLI reads `COFACTOR_API_TOKEN` from the environment (no login step);\n only the steps that call Cofactor get it, and `validate` needs none.\n The build step\'s cache settings are the ones under FAST RE-PUSHES.\n `--wait` follows the import for up to 50 minutes and fails the job if\n Cofactor cannot boot the image. A push still importing after 50\n minutes is not a failure: the job moves on and the suite runs on the\n previous head. The suite runs on the application\'s head: the newest push\n tagged `main` or `nightly`. Start the workflow by hand with "Run\n workflow" (the `workflow_dispatch` trigger) to test a change to this\n file. `timeout-minutes` covers a build, the longest import wait and the\n suite; raise it if your suite runs longer.\n\n The last step is optional: a suite can also run on a Cofactor schedule.\n\nMAKING YOUR IMAGE SYNCABLE\n\n `cofactor sandbox sync` writes your working tree into a claimed sandbox\n (see `guide v2 local`). For a pushed image it syncs the directory holding\n cofactor.sandbox.yaml into the image\'s WORKDIR, so the image must keep\n that directory\'s files at WORKDIR in the same layout. The rule: a\n syncable image has your definition file at its WORKDIR, as `COPY . .`\n gives you.\n\n WORKDIR /app\n COPY . .\n RUN npm ci && npm run build\n\n A multi-stage build that copies only some directories works too, as\n long as each lands at the same relative path under WORKDIR and the\n definition file is copied in:\n WORKDIR /app\n COPY --from=build /src/api /app/api\n COPY cofactor.sandbox.yaml /app/cofactor.sandbox.yaml\n\n Files the image doesn\'t ship (sources it compiles away, docs) are fine:\n `sync` writes them as new files and nothing runs them.\n\n Compiled output isn\'t rebuilt in the sandbox (nothing builds at push or claim; `sync --build` reruns a declared `lifecycle.build`):\n build it locally, then sync the result to where the image serves it:\n npm run build\n cofactor sandbox sync <id> dist --to public\n\n `push` checks the rule on the local image and says one of:\n Sync: syncable.\n Sync: syncable (the image is older than your working tree, which is fine).\n Sync: not syncable: cofactor.sandbox.yaml isn\'t at /app/cofactor.sandbox.yaml (...)\n Sync: not checked (...) (Docker, or git, couldn\'t answer)\n It never stops a push. It reads the definition at your WORKDIR, not the\n copy `push` adds for Cofactor itself.\n\nHOW YOUR IMAGE RUNS IN A SANDBOX\n\n - Its ENTRYPOINT and CMD are NOT used. Start each process yourself with a\n `services.<name>.run` command (often your entrypoint script).\n - Its ENV and WORKDIR ARE applied, as `docker run` would, its PATH\n included: commands run through a `bash -lc` login shell, and the image\'s\n own PATH is put back in front of whatever the login profile sets.\n - Leave the image\'s USER as root (or unset). The sandbox starts as root\n and drops to uid 1000 itself; an image that ended on another `USER`\n has failed to boot with a permission error on `/etc/cofactor`.\n - Everything \u2014 services, prepare commands, actions \u2014 runs as an\n unprivileged user with uid 1000 (the sandbox adds that account if your\n image has none). The WORKDIR tree is made theirs; anything the app\n writes must be under the WORKDIR or `/tmp`, not elsewhere in a root-\n owned path.\n - One machine, no Docker inside: a database is a process you run as a\n service (Postgres installed in the image, or SQLite), not a container.\n For S3 or GCS, declare `object_storage` and the sandbox runs the\n server for you (see the `object-storage` topic).\n - Each public port gets its own https URL, unique per sandbox. Its\n origin is a param \u2014 `params: { web_url: { port: web } }` \u2014 that you\n pass to the app through `env` (e.g. `PUBLIC_URL: ${params.web_url}`).\n\nTHE DEFINITION FOR PUSHED IMAGES\n\n The same `cofactor.sandbox.yaml` as for a repository (see the\n `definition` topic), minus installing the toolchain: no `setup`. `lifecycle.build`\n and `after_build` are optional and run only on `sync --build`. A minimal one for the example app,\n built as one image that serves its UI and API on port 8080:\n\n version: 1\n resources: { vcpus: 2, memory_mib: 4096 }\n ports:\n web: { port: 8080, access: public }\n params:\n web_url: { port: web }\n env:\n PUBLIC_URL: ${params.web_url}\n services:\n app:\n run: exec ./start.sh\n health: { http: 8080, path: /health }\n reload_on_prepare: true\n personas:\n owner: { email: owner@example.test }\n lifecycle:\n prepare:\n - bash sandbox/personas.sh\n actions:\n create-workspace:\n description: A new workspace, created by the given persona, who becomes its owner.\n run: sandbox/actions/create-workspace.sh\n input:\n name: { type: string, required: true }\n as: { type: persona, required: true }\n output:\n workspace_id: { type: string }\n environments:\n default:\n description: Workspace "Acme", owned by the owner persona.\n seeds:\n - id: acme\n action: create-workspace\n input: { name: Acme, as: owner }\n\n From here, add what your tests need, in the order "WHAT TO BUILD FIRST"\n in the `seeds-and-actions` topic gives: a persona per role, an\n environment per plan or flag combination, and actions for the core\n objects.\n\n Keep the file anywhere in your repo: `validate --upload <file>` and\n `push --definition <file>` read it from where you point them, and the\n push puts it in the image. Paths inside it (`run:` of actions, prepare\n commands) are relative to the WORKDIR, so ship your sandbox scripts in\n the image (e.g. `COPY sandbox/ sandbox/`).\n\n Services start when the image is first booted, BEFORE any sandbox exists,\n with params at placeholder values. A service that needs the sandbox\'s own\n values (its URL, a `{secret: true}` param) or reads a secret sets\n `reload_on_prepare: true`: it is started (and restarted) by each\n sandbox\'s prepare instead. In doubt, set it on your app\'s service.\n\n `health` passes when the http path answers below 400 (or the command\n exits 0) within 5 minutes of the service starting.\n\nPERSONAS: MAKING THE LOGINS WORK\n\n Cofactor generates a fresh password for every persona when a snapshot is\n built; every sandbox copied from it has the same ones. Your\n `lifecycle.prepare` commands (which also run on each copy) receive them as `COFACTOR_PERSONAS`, a\n JSON map `{ "<persona>": { "email": "...", "password": "..." } }`, and\n must make each login work in your app: create or update that user with\n that password (through your app\'s API, its CLI, or its database). A\n persona whose login doesn\'t work breaks every test that signs in as it.\n A persona\'s `email` is yours to choose: pick an account your app already\n has (its seed data) and set its password, or have prepare create it.\n Cofactor\'s test runs then sign in with these logins automatically, as\n the application\'s persona of the same name.\n\n Prepare runs while your app is up: the `reload_on_prepare` services are\n running and healthy (restarted first when their environment changed),\n the others have run since the image was booted. So write the logins\n somewhere the running app reads on every sign-in (its database, or its\n own API). If your app loads its accounts once at startup (a file or\n config read into memory), change it to read them per request, or the new\n passwords won\'t take. For an app whose logins are fixtures, a common fix\n is a small JSON file the prepare script writes from `COFACTOR_PERSONAS`\n and the app checks on each sign-in before its fixtures.\n\nACTIONS AND SEEDS\n\n An action is a script in the image with typed input and output (the\n `seeds-and-actions` topic covers designing them). Inputs arrive as\n `INPUT_<NAME>` env vars (a `persona` input carries the persona\'s name,\n e.g. `owner`), alongside the definition\'s `env:`, every param as\n `COFACTOR_PARAM_<NAME>`, and `COFACTOR_PERSONAS`. The script prints its\n result as a JSON object on the LAST stdout line that starts with `{`\n (send everything else to stderr). It runs inside the sandbox, so it\n reaches your app on localhost. An environment\'s `seeds` call actions in\n order, once per version, before the snapshot every claim copies;\n `${<step id>.<output>}` passes one step\'s output to a later step. Run\n one against a live sandbox with `cofactor sandbox run <id> <action>\n --input name=... --input as=owner`.\n\n An action that signs in as a persona and creates data through the app\'s\n own API (here a cookie-session JSON API on port 8080; the port, paths and\n JSON fields are the example app\'s, use your app\'s). It needs jq and\n curl, which slim base images lack: add them to your Dockerfile\n (`apt-get install -y --no-install-recommends jq curl`).\n\n #!/usr/bin/env bash\n # sandbox/actions/create-workspace.sh\n set -euo pipefail\n login=$(jq -c --arg p "$INPUT_AS" \'.[$p] | {email, password}\' <<<"$COFACTOR_PERSONAS")\n jar=$(mktemp)\n curl -fsS -c "$jar" -H \'content-type: application/json\' \\\n -d "$login" http://127.0.0.1:8080/api/auth/login >&2\n id=$(curl -fsS -b "$jar" -H \'content-type: application/json\' \\\n -d "$(jq -nc --arg n "$INPUT_NAME" \'{name: $n}\')" \\\n http://127.0.0.1:8080/api/workspaces | jq -er \'.id\')\n jq -nc --arg id "$id" \'{workspace_id: $id}\'\n\n\nTROUBLESHOOTING\n\n - `push` says the image is not linux/amd64, or its WORKDIR can\'t be\n used: rebuild as it says.\n - The push fails with "Cofactor could not start this image": a service\n exited or never turned healthy. The message ends with that service\'s\n log. To reproduce, open a shell in the image \u2014\n docker run --rm -it --entrypoint bash <image>\n \u2014 and run that service\'s `run` command from the WORKDIR. In the\n sandbox it runs as uid 1000 with the WORKDIR made theirs; plain\n `docker run --user 1000` leaves the WORKDIR root-owned, so a write\n failure there is not the sandbox\'s.\n - A claim fails in prepare or a seed: the error carries the command\'s\n output. Fix the script, rebuild the image and push again.\n - To try prepare and actions before pushing, run the app as usual and\n call your scripts in it with the variables they read (a sandbox also\n sets the definition\'s `env:` and runs them as uid 1000):\n docker run -d --name try -p 8080:8080 <image>\n docker exec -e COFACTOR_PERSONAS=\'{"owner":{"email":"owner@example.test","password":"pw-1"}}\' try bash sandbox/personas.sh\n docker exec -e COFACTOR_PERSONAS=\'\u2026\' -e INPUT_NAME=Acme -e INPUT_AS=owner try bash sandbox/actions/create-workspace.sh\n (`docker exec` starts in the image\'s WORKDIR.)\n - `cofactor sandbox logs` and `diagnostics` are for the older sandboxes;\n for these, `cofactor sandbox show <id>` has the status and any error.\n - "push the image again": Cofactor\'s sandbox helper changed since that\n push; a fresh `cofactor sandbox push` picks up the current one.\n - A layer larger than 5 GB is refused; split or slim it.\n - Every push uploads the whole image: the build cache is missing (see\n FAST RE-PUSHES), or the Dockerfile copies the source before\n installing dependencies.\n';
|
|
322
323
|
|
|
323
324
|
// ../content/src/sandbox-v2-guide/local.txt
|
|
324
325
|
var local_default = "YOUR UNCOMMITTED CHANGES IN A SANDBOX (SYNC)\n\nWrite a test next to your code and run it against your real app, before\nyou commit: claim a sandbox, sync your working tree into it, rebuild what\nthe change needs, and open it or run a flow on it. No commit, no image\nbuild, no pull request.\n\n cofactor sandbox claim --application \"Project Tracker\" --wait\n # edit files\n cofactor sandbox sync <id>\n cofactor sandbox exec <id> -- cofactor-sandbox restart app\n cofactor workflow run <flow> --sandbox <id>\n # edit, and sync again\n\nYou decide what to rebuild and restart: a sync only writes and deletes\nfiles. Each sync extends the sandbox's lease by up to 2 hours, up to 8 hours\nafter the claim. Syncing needs the `sandbox:exec` scope for API keys.\n\nWHAT A SYNC DOES\n\n `sync` makes the sandbox's files match your working tree, the way git\n sees it: tracked files with your uncommitted edits, plus untracked files\n that aren't ignored. Ignored files (node_modules/, .env if you ignore it,\n build output) are not synced. Your git index is never touched.\n\n - GitHub application: your repository's top level syncs into the\n sandbox's checkout. Run `sync` anywhere inside the repository.\n - Pushed image: run `sync` from the directory holding\n cofactor.sandbox.yaml; it syncs into the image's WORKDIR (see\n \"Making your image syncable\" in `guide v2 upload`).\n\n Before it writes anything, `sync` prints the files it will write (each\n untracked one marked), the files it will delete, and what it keeps. Check\n for a stray untracked file you didn't mean to send, such as an unignored\n .env.\n\n It ends with the synced tree id, which `cofactor sandbox show` and every\n flow run on the sandbox record. A later commit of exactly these files has\n the same tree (`git rev-parse <commit>^{tree}`, or `<commit>:<dir>` for a\n pushed image's directory).\n\nDELETIONS\n\n A file only the sandbox has is deleted only when all three hold:\n - it isn't in your working tree,\n - your ignore rules don't match it, and\n - some commit in your local history once had a file at that path.\n So a file you deleted, or a directory you removed, is deleted in the\n sandbox, while everything your image or build put there (node_modules/,\n .venv/, compiled assets, a script your Dockerfile copied elsewhere) is\n kept. `sync` lists both.\n\n Syncing again is always safe: it compares against the sandbox's real\n files, so a sync that stopped part-way converges on the next one.\n\nREBUILD AND RESTART\n\n Services keep running the old code until you restart them:\n cofactor sandbox exec <id> -- cofactor-sandbox restart app\n `restart` names one or more services, waits until they're healthy, and\n prints each one's status. An unknown name lists the sandbox's services.\n\n Install dependencies, run migrations or a build with `exec` first: it runs\n in the same directory and environment your services use (the definition's\n directory in the checkout, or a pushed image's WORKDIR):\n cofactor sandbox exec <id> -- npm ci\n cofactor sandbox exec <id> -- npx prisma migrate deploy\n\n Logs: each service writes /var/lib/cofactor/logs/<service>.log.\n cofactor sandbox exec <id> -- tail -n 100 /var/lib/cofactor/logs/app.log\n\n Let the definition do it (GitHub and pushed applications alike):\n cofactor sandbox sync <id> --build\n reruns each `lifecycle.build` step whose `inputs` changed (and every step\n after it), then restarts the `after_build` services. Only this flag runs\n them: a sync without it, a push and a claim never do. A pushed image's\n steps run in its WORKDIR and come from the definition baked into the image,\n so changing them takes a new `push`. The first `--build` on a claimed\n sandbox has no record of earlier runs, so it runs every step.\n\nCOMPILED OUTPUT\n\n When the sandbox runs compiled output (a bundled frontend, compiled\n TypeScript), build it locally and sync the result to where the sandbox\n serves it:\n bun run build\n cofactor sandbox sync <id> frontend/dist --to static\n `--to` is relative to the sync root (the checkout, or the image's\n WORKDIR). This mirrors the local directory as is: ignore rules don't\n apply, so a git-ignored dist/ is sent; every file in it is written, and\n every other file under --to is deleted.\n `show` and runs then record it as build output.\n\nDATA IS FORWARD-ONLY\n\n A sync changes files, never data. A migration you ran stays applied when\n you sync older files back, and seeded rows stay. To start over, destroy\n the sandbox and claim a new one.\n\nRUN A FLOW ON IT\n\n cofactor workflow run <flow> --sandbox <id>\n\n The run uses this sandbox instead of claiming a fresh copy, and never\n destroys it. The run targets the sandbox's own application and\n environment (no --environment needed; naming another one is refused), the\n sandbox must be ready, and it must be one you claimed, not a test run's. The run records the version and the synced tree it ran\n against, as of when it started; `cofactor workflow debug overview <run>`\n shows them. Syncing during a run changes the files under it, so sync\n between runs.\n\nLIMITS\n\n - Sync only sandboxes claimed with `cofactor sandbox claim`; a test run's\n own copy can't be synced.\n - Symlinks and submodules are not synced (`sync` lists them).\n - A single file larger than 63 MiB is refused; a sync of many files is\n sent in several requests.\n - The `sync` port counts toward the 8-port limit (see `guide v2 ports`).\n";
|
|
@@ -866,6 +867,16 @@ ACTION
|
|
|
866
867
|
- declaredOutputs: values to extract from the page for downstream use
|
|
867
868
|
- customInstructions: additional guidance for the agent executing the step
|
|
868
869
|
|
|
870
|
+
EDITING A NODE
|
|
871
|
+
\`cofactor workflow node patch <workflowId> <nodeId> --config '{...}'\` merges the
|
|
872
|
+
JSON into the node's configJson one level deep, so a nested object is replaced
|
|
873
|
+
whole: {"params":{"statement":"..."}} changes a sandbox_action's SQL and
|
|
874
|
+
leaves its actionSlug and outputKeys alone, but drops any other params key,
|
|
875
|
+
so list every param to keep. Copy the key path from \`workflow get\`.
|
|
876
|
+
\`--set-input key=value\` writes configJson.inputValues, which only action, seed
|
|
877
|
+
and call_flow nodes read, and only for inputs they declare. On any other node
|
|
878
|
+
type the value is never used: use --config.
|
|
879
|
+
|
|
869
880
|
ASSERTION
|
|
870
881
|
Verifies expected outcomes. Define a condition to check.
|
|
871
882
|
What happens on failure depends on the severity.
|
|
@@ -2422,11 +2433,14 @@ var cli_seeds_default = 'Use this section when you need to create or inspect see
|
|
|
2422
2433
|
// ../content/src/workflow-guide/cli-api-sequences.txt
|
|
2423
2434
|
var cli_api_sequences_default = 'Use this section when you need to drive API sequence authoring from the\nCofactor CLI. The conceptual model lives in `api-sequences`; this section is\nonly command orientation. Prefer each command\'s `--help` output for the\ncurrent flag list.\n\nCOMMON FLOW\n\n 1. Inspect the API catalog for candidate endpoints.\n\n cofactor api-catalog sources list\n cofactor api-catalog endpoints list --search <keyword>\n cofactor api-catalog endpoints get <apiRequestId>\n\n 2. Compose a sequence JSON file locally. Reference endpoints by\n apiRequestId and chain steps with {{stepId.response.path}}.\n\n 3. Create an API-only workflow from the sequence file.\n\n cofactor workflow create --api-only --name "smoke: create project" \\\n --sequence-from-file seq.json\n\n The command returns the workflow, sequence, and node IDs.\n\n To attach a sequence to an existing workflow that has none, run\n `cofactor workflow api-sequence update <workflowId> --from-file seq.json`;\n it creates the sequence when absent and replaces the config otherwise.\n\n 4. Execute immediately to surface template warnings and API failures.\n\n cofactor workflow api-sequence execute <workflowId>\n\n 5. Inspect the run and iterate on the sequence file.\n\n cofactor workflow runs list <workflowId> --side api\n cofactor workflow runs get <workflowId> <runId>\n cofactor workflow runs get <workflowId> <runId> --step <runStepId> --api-call <apiStepId>\n Add --json for JSON; the flags print stored (capped) request/response bodies.\n Get <runStepId> from runs get <workflowId> <runId> --json (steps[].id).\n cofactor workflow api-sequence update <workflowId> --from-file seq.json\n\nREFERENCE\n\n - Command help: `cofactor workflow api-sequence --help`\n - Workflow creation help: `cofactor workflow create --help`\n - API catalog help: `cofactor api-catalog --help`\n';
|
|
2424
2435
|
|
|
2436
|
+
// ../content/src/workflow-guide/triage-results.txt
|
|
2437
|
+
var triage_results_default = "Find out what failed, whether a suite is healthy, and what to do about a red\nrun. Read results first, then decide; do not guess from a status alone.\n\nRECENT SUITE RUNS\n\n cofactor test-suite executions\n cofactor test-suite get <executionId> --failures-only\n cofactor test-suite get <executionId> --failures-only --json\n\n `executions` lists recent suite runs; `get` shows one, and --failures-only\n keeps just the failing test runs. The execution id comes from `executions`,\n from `cofactor test-suite run`, or from a run page.\n\nONE FAILING TEST\n\n cofactor test-suite diagnostics <workflowId> <runId>\n cofactor test-suite diagnostics <workflowId> <runId> --thoughts\n cofactor workflow runs list <workflowId>\n\n `diagnostics` returns the failure diagnostic for one run (--thoughts adds the\n agent's reasoning trace). `workflow runs list` is the run history of one\n test: use it to see whether the failure is new, intermittent or constant.\n\nRUNS FROM YOUR OWN CI\n\n Results from a framework Cofactor ran for you (not a Cofactor Workflow) are\n external runs. Read `cofactor external-run guide` for get, failures, triage\n and rerun. For how tests are structured, read the other sections of this\n guide (`cofactor workflow guide`).\n\nREAL FAILURE OR FLAKE\n\n 1. A failure is real only when it reproduces with the same evidence: the\n same step, the same error, in more than one run.\n 2. An intermittent failure with different errors each time is a flake\n candidate. Check the run history before calling it one.\n 3. A failure that began right after a change is a real regression until\n shown otherwise.\n\nNEXT STEP\n\n - Real and new: file or find the issue (`cofactor issues guide`).\n - One unexplained failure: read the run's evidence (the diagnostic above),\n then hand it to `cofactor-debug-run` rather than guessing.\n - Flake: say so with the run history as evidence; do not delete the test.\n";
|
|
2438
|
+
|
|
2425
2439
|
// ../content/src/workflow-guide/write-a-test.txt
|
|
2426
|
-
var write_a_test_default = 'Use this section to author a test straight from the CLI: plan the journey, check\nfor duplicates, create or update the workflow, run it, and hand back its URL.\nNo spec files are needed.\n\nAUTHORING LOOP\n\n 1. Understand the requirement. Read the ticket, PRD, or the user\'s intent, and\n explore the real UI first (see `best-practices` and `cofactor session guide`).\n 2. Avoid duplicates:\n cofactor workflow list --search "<keywords>"\n If a workflow already covers the journey, update it instead of creating one.\n 3. Create it (JSON on stdin or from a file; shapes are in `node-types`):\n cofactor workflow create --from-file test.json\n cofactor workflow create --from-file test.json --tags "p0,checkout"\n Input: { "name": "...", "nodes": [{ "nodeType": "...", "configJson": {...} }] }\n A node has nodeType, configJson and, for action nodes, a required "title"\n (plus id/orderIndex when reusing). The title is short and says what the user\n does; the detail goes in configJson.actionDescription:\n { "nodeType": "action", "title": "Create a ticket",\n "configJson": { "actionDescription": "Fill in the required fields and save" } }\n The title is what the run timeline, canvas and failure reports show. An\n action node without one is rejected: add "title": "<short goal>" to it.\n To act as a persona, make the first node a login node:\n {"nodeType": "login", "configJson": {"credentialSource": "from_template",\n "credentialTemplateName": "<persona>", "performLogin": true}}\n Use credentialTemplateId instead of the name when the persona name exists on\n more than one application. Without credentialSource "from_template" a login\n node expects a stored credentialId.\n 4. Change an existing workflow:\n cofactor workflow get <workflowId> (read it first)\n cofactor workflow sync <workflowId> --from-file test.json (replace all nodes)\n cofactor workflow node patch <workflowId> <nodeId> --set-input key=value\n cofactor workflow patch <workflowId> --name "..." --description "..."\n cofactor workflow patch <workflowId> --preflight @preflight.md --cleanup @cleanup.md\n Reuse each existing node `id` in sync to keep a minimal diff. Every new or\n edited action step needs a "title"; an untitled legacy step you leave exactly\n as it is stays accepted. `workflow get --json` lists "title": null for such steps.\n Add --propose to sync to open a proposal for review instead of applying directly.\n 5. Run it and look at the result:\n cofactor workflow run <workflowId> --stream\n cofactor workflow runs list <workflowId>\n cofactor workflow debug <runId> (when a run fails)\n To run on a sandbox you claimed: `cofactor workflow run <workflowId>\n --sandbox <sandboxId>`. The run targets the sandbox\'s own environment, so\n no --environment is needed. `cofactor sandbox claims` lists your live\n claims (ids, expiry) if you lost an id; `cofactor sandbox destroy <id>`.\n 6. Return the workflow URL printed by the CLI to the user verbatim. Never\n build the URL yourself.\n\n Approving a test asserts that a human verified it checks the right thing. Do\n not pass --yes to `workflow approve` until the user has confirmed.\n\nTHE RUBRIC: EVERY TEST MUST SATISFY ALL OF THESE\n\n 1. One coherent journey per workflow\n One user objective, possibly with several verifications along the way.\n "Atomic" means coherent, not minimal. If isolating a check would force you\n to duplicate most of another test\'s setup, fold it in as an inline\n assertion on the larger journey.\n Good: create org -> invite a user -> claim invite -> land on dashboard.\n Good: submit a ticket and confirm it appears in the queue.\n Bad: submit a ticket, edit a user, generate a report (three journeys,\n three workflows).\n Split only when the setup is genuinely different or the failure modes are\n independent. A long single journey stays together; independent journeys\n become separate workflows so they run in parallel and fail independently.\n\n 2. Specific, observable expected results\n Every assertion is something a stranger could verify without judgement,\n and comes from the requirement or confirmed user intent, not incidental\n DOM details seen while exploring. Keep conditions about user-visible\n behavior; do not invent ARIA/SVG checks or pixel thresholds.\n Good: "URL ends with /dashboard." "A row named X appears in the table."\n Bad: "The page looks correct." "It works."\n Attach assertions inline to the action that produced the result (see\n `assertions`). Pick severity deliberately: fatal ends the run (gating\n preconditions), fail marks the run failed but continues (feature claims),\n warn records without failing (cosmetics). A test with no run-failing\n assertion proves nothing.\n Relative checks ("the count dropped by 2") must reference a value captured\n upstream and used via $var.<name>, never rely on memory of the earlier state.\n\n 3. Idempotent, self-owned, isolated\n - Rerunning passes without a manual reset: use macros ($EMAIL(), $UUID(),\n "QA Test $RANDOM_STRING(6)") for anything that must be unique (see `macros`).\n - The test creates the records it operates on, inline. A seed workflow is\n for data that several tests share; a seed with one consumer is overhead.\n Data the test only reads may pre-exist, but never mutate it.\n - Never modify shared resources (billing, integrations, API keys, org\n config) as a side effect.\n - Tests that depend on one another run as ordered suite layers, never by\n implicit ordering.\n - A fatal assertion halts the run before trailing cleanup. Anything that\n must be gone after a failure belongs in a seed with a teardown (see\n `seed-workflows`, `data-lifecycle`).\n\n 4. Traceable\n Link the test to the product-map capability it proves:\n cofactor product-map link <CAP-code> <workflowId>\n Traceability comes from that link, not from text: do not write capability\n codes, ticket ids or requirement references into the name or description.\n If no capability covers the test, tell the user; untraceable tests rot.\n\n 5. Risk-prioritized\n Tag each test p0, p1, or p2 with --tags / --tags-add:\n p0 happy path of the core promise; if it breaks the feature is broken\n p1 primary edge cases, validation, role variants, common error states\n p2 rare conditions, visual or agentic checks, low-frequency edges\n\n 6. Preconditions, data, and cleanup are explicit\n Auth and role: use a persona (cofactor persona list --application "<app>")\n rather than raw usernames and passwords. Shared setup: a seed node, placed\n before login. Test data: workflow inputs with macros. Anything else the\n test depends on (pre-existing data it reads, flags, third-party systems)\n goes in the workflow\'s preflight, and how its side effects get reset goes\n in its cleanup. Write each as a short markdown bullet list:\n cofactor workflow patch <workflowId> --preflight @preflight.md\n cofactor workflow patch <workflowId> --cleanup @cleanup.md\n\n 7. The description says what the test is for\n Read together with the name, the description gives the spirit of the\n test: what we are doing and why it matters, in one or two plain\n sentences. It never holds capability codes, preconditions, issue ids,\n status ("expected red", "blocked"), step narration, endpoints, payloads,\n timings or cache details. Each of those lives elsewhere: the product-map\n link, the preflight, the linked issue, the run history, the nodes.\n People read the description, preflight and cleanup in the web UI, so\n never put a raw ID in them: no UUID, UUID prefix, wf_/prule_ or other\n prefixed id, issue id or run id. Name the thing instead (the seed\n workflow\'s name, the teardown\'s name, the connection\'s name, the\n record\'s name).\n Good: name "Invite a teammate to Acme"\n description "An admin invites a new teammate by email and the\n teammate can sign in. Growing a team depends on invites\n reaching people."\n Bad: description "CAP-ORG-004. Needs seed org + admin persona. Steps:\n open /settings/team, POST /api/invites, wait 5s, check inbox.\n Expected red until ISSUE-812 ships. Cache warm."\n Bad preflight:\n Seed 5e055090 creates an empty layer. Paired teardown d9a44386\n deletes the flow.\n Good preflight:\n - The "Acme empty layer" seed creates an empty layer\n - Its paired teardown, "Acme empty layer teardown", deletes the flow\n\n 8. Written as human intent, not UI mechanics\n Action titles say what the user is trying to do ("Assign the ticket to\n Support"), not click-by-click steps. Each action is one coherent,\n assertable step: usually broader than one click, ends at a visible\n boundary (modal gone, URL changed, row appeared), and has no conditionals\n inside it. If the data might not exist, seed it instead of branching.\n\nCHOOSING VALUES\n\n - Names existing data in the target app (a property, persona, credential, a\n seeded record): do not guess. Look it up with `cofactor property list`,\n `cofactor persona list --application "<app>"`, or\n `cofactor workflow list --search <term>`, and ask the user when several\n candidates fit.\n - Synthesizable (email, UUID, throwaway string, date): use a macro and move on.\n - Auth or role: prefer a persona over a raw credential.\n\nCOVERAGE CHECKLIST\n\n Before handing work back, for each item either include a test or say why it\n does not apply: happy path (p0); primary edges and boundary inputs; negative\n paths and validation errors; permission and role variants; empty, loading,\n and error states; cross-entity side effects (does this change something\n visible elsewhere?); cleanup or undo.\n\n Aim for the smallest set that proves the feature works. Five sharp tests beat\n twenty fuzzy ones.\n';
|
|
2440
|
+
var write_a_test_default = 'Use this section to author a test straight from the CLI: plan the journey, check\nfor duplicates, create or update the workflow, run it, and hand back its URL.\nNo spec files are needed.\n\nAUTHORING LOOP\n\n 1. Understand the requirement. Read the ticket, PRD, or the user\'s intent, and\n explore the real UI first (see `best-practices` and `cofactor session guide`).\n 2. Avoid duplicates:\n cofactor workflow list --search "<keywords>"\n If a workflow already covers the journey, update it instead of creating one.\n 3. Create it (JSON on stdin or from a file; shapes are in `node-types`):\n cofactor workflow create --from-file test.json\n cofactor workflow create --from-file test.json --tags "p0,checkout"\n Input: { "name": "...", "nodes": [{ "nodeType": "...", "configJson": {...} }] }\n A node has nodeType, configJson and, for action nodes, a required "title"\n (plus id/orderIndex when reusing). The title is short and says what the user\n does; the detail goes in configJson.actionDescription:\n { "nodeType": "action", "title": "Create a ticket",\n "configJson": { "actionDescription": "Fill in the required fields and save" } }\n The title is what the run timeline, canvas and failure reports show. An\n action node without one is rejected: add "title": "<short goal>" to it.\n To act as a persona, make the first node a login node:\n {"nodeType": "login", "configJson": {"credentialSource": "from_template",\n "credentialTemplateName": "<persona>", "performLogin": true}}\n Use credentialTemplateId instead of the name when the persona name exists on\n more than one application. Without credentialSource "from_template" a login\n node expects a stored credentialId.\n 4. Change an existing workflow:\n cofactor workflow get <workflowId> (read it first)\n cofactor workflow sync <workflowId> --from-file test.json (replace all nodes)\n cofactor workflow node patch <workflowId> <nodeId> --config \'{"params":{"statement":"..."}}\'\n cofactor workflow patch <workflowId> --name "..." --description "..."\n cofactor workflow patch <workflowId> --preflight @preflight.md --cleanup @cleanup.md\n Reuse each existing node `id` in sync to keep a minimal diff. Every new or\n edited action step needs a "title"; an untitled legacy step you leave exactly\n as it is stays accepted. `workflow get --json` lists "title": null for such steps.\n Add --propose to sync to open a proposal for review instead of applying directly.\n `node patch --config` merges configJson one level deep: name the node\'s own\n keys (a sandbox_action\'s SQL is {"params":{"statement":"..."}}; an action\'s\n text is {"actionDescription":"..."}). A nested object is replaced whole, so\n "params" must list every param to keep. Run `workflow get` first and copy the\n key path from the node\'s configJson. `--set-input key=value` writes\n configJson.inputValues, which only action, seed and call_flow nodes read, and\n only for inputs the node (or the called flow) declares. On every other node\n type the value is never used: use --config.\n[cli >= 0.55.0]\n The CLI rejects --set-input on node types that ignore inputValues, and on an\n action input the node has no data requirement for.\n For multi-line SQL or prompts, put the JSON in a file: --config @patch.json\n[/cli]\n 5. Run it and look at the result:\n cofactor workflow run <workflowId> --stream\n cofactor workflow runs list <workflowId>\n cofactor workflow debug <runId> (when a run fails)\n To run on a sandbox you claimed: `cofactor workflow run <workflowId>\n --sandbox <sandboxId>`. The run targets the sandbox\'s own environment, so\n no --environment is needed. `cofactor sandbox claims` lists your live\n claims (ids, expiry) if you lost an id; `cofactor sandbox destroy <id>`.\n 6. Return the workflow URL printed by the CLI to the user verbatim. Never\n build the URL yourself.\n\n Approving a test asserts that a human verified it checks the right thing. Do\n not pass --yes to `workflow approve` until the user has confirmed.\n\nTHE RUBRIC: EVERY TEST MUST SATISFY ALL OF THESE\n\n 1. One coherent journey per workflow\n One user objective, possibly with several verifications along the way.\n "Atomic" means coherent, not minimal. If isolating a check would force you\n to duplicate most of another test\'s setup, fold it in as an inline\n assertion on the larger journey.\n Good: create org -> invite a user -> claim invite -> land on dashboard.\n Good: submit a ticket and confirm it appears in the queue.\n Bad: submit a ticket, edit a user, generate a report (three journeys,\n three workflows).\n Split only when the setup is genuinely different or the failure modes are\n independent. A long single journey stays together; independent journeys\n become separate workflows so they run in parallel and fail independently.\n\n 2. Specific, observable expected results\n Every assertion is something a stranger could verify without judgement,\n and comes from the requirement or confirmed user intent, not incidental\n DOM details seen while exploring. Keep conditions about user-visible\n behavior; do not invent ARIA/SVG checks or pixel thresholds.\n Good: "URL ends with /dashboard." "A row named X appears in the table."\n Bad: "The page looks correct." "It works."\n Attach assertions inline to the action that produced the result (see\n `assertions`). Pick severity deliberately: fatal ends the run (gating\n preconditions), fail marks the run failed but continues (feature claims),\n warn records without failing (cosmetics). A test with no run-failing\n assertion proves nothing.\n Relative checks ("the count dropped by 2") must reference a value captured\n upstream and used via $var.<name>, never rely on memory of the earlier state.\n\n 3. Idempotent, self-owned, isolated\n - Rerunning passes without a manual reset: use macros ($EMAIL(), $UUID(),\n "QA Test $RANDOM_STRING(6)") for anything that must be unique (see `macros`).\n - The test creates the records it operates on, inline. A seed workflow is\n for data that several tests share; a seed with one consumer is overhead.\n Data the test only reads may pre-exist, but never mutate it.\n - Never modify shared resources (billing, integrations, API keys, org\n config) as a side effect.\n - Tests that depend on one another run as ordered suite layers, never by\n implicit ordering.\n - A fatal assertion halts the run before trailing cleanup. Anything that\n must be gone after a failure belongs in a seed with a teardown (see\n `seed-workflows`, `data-lifecycle`).\n\n 4. Traceable\n Link the test to the product-map capability it proves:\n cofactor product-map link <CAP-code> <workflowId>\n Traceability comes from that link, not from text: do not write capability\n codes, ticket ids or requirement references into the name or description.\n If no capability covers the test, tell the user; untraceable tests rot.\n\n 5. Risk-prioritized\n Tag each test p0, p1, or p2 with --tags / --tags-add:\n p0 happy path of the core promise; if it breaks the feature is broken\n p1 primary edge cases, validation, role variants, common error states\n p2 rare conditions, visual or agentic checks, low-frequency edges\n\n 6. Preconditions, data, and cleanup are explicit\n Auth and role: use a persona (cofactor persona list --application "<app>")\n rather than raw usernames and passwords. Shared setup: a seed node, placed\n before login. Test data: workflow inputs with macros. Anything else the\n test depends on (pre-existing data it reads, flags, third-party systems)\n goes in the workflow\'s preflight, and how its side effects get reset goes\n in its cleanup. Write each as a short markdown bullet list:\n cofactor workflow patch <workflowId> --preflight @preflight.md\n cofactor workflow patch <workflowId> --cleanup @cleanup.md\n\n 7. The description says what the test is for\n Read together with the name, the description gives the spirit of the\n test: what we are doing and why it matters, in one or two plain\n sentences. It never holds capability codes, preconditions, issue ids,\n status ("expected red", "blocked"), step narration, endpoints, payloads,\n timings or cache details. Each of those lives elsewhere: the product-map\n link, the preflight, the linked issue, the run history, the nodes.\n People read the description, preflight and cleanup in the web UI, so\n never put a raw ID in them: no UUID, UUID prefix, wf_/prule_ or other\n prefixed id, issue id or run id. Name the thing instead (the seed\n workflow\'s name, the teardown\'s name, the connection\'s name, the\n record\'s name).\n Good: name "Invite a teammate to Acme"\n description "An admin invites a new teammate by email and the\n teammate can sign in. Growing a team depends on invites\n reaching people."\n Bad: description "CAP-ORG-004. Needs seed org + admin persona. Steps:\n open /settings/team, POST /api/invites, wait 5s, check inbox.\n Expected red until ISSUE-812 ships. Cache warm."\n Bad preflight:\n Seed 5e055090 creates an empty layer. Paired teardown d9a44386\n deletes the flow.\n Good preflight:\n - The "Acme empty layer" seed creates an empty layer\n - Its paired teardown, "Acme empty layer teardown", deletes the flow\n\n 8. Written as human intent, not UI mechanics\n Action titles say what the user is trying to do ("Assign the ticket to\n Support"), not click-by-click steps. Each action is one coherent,\n assertable step: usually broader than one click, ends at a visible\n boundary (modal gone, URL changed, row appeared), and has no conditionals\n inside it. If the data might not exist, seed it instead of branching.\n\nCHOOSING VALUES\n\n - Names existing data in the target app (a property, persona, credential, a\n seeded record): do not guess. Look it up with `cofactor property list`,\n `cofactor persona list --application "<app>"`, or\n `cofactor workflow list --search <term>`, and ask the user when several\n candidates fit.\n - Synthesizable (email, UUID, throwaway string, date): use a macro and move on.\n - Auth or role: prefer a persona over a raw credential.\n\nCOVERAGE CHECKLIST\n\n Before handing work back, for each item either include a test or say why it\n does not apply: happy path (p0); primary edges and boundary inputs; negative\n paths and validation errors; permission and role variants; empty, loading,\n and error states; cross-entity side effects (does this change something\n visible elsewhere?); cleanup or undo.\n\n Aim for the smallest set that proves the feature works. Five sharp tests beat\n twenty fuzzy ones.\n';
|
|
2427
2441
|
|
|
2428
2442
|
// ../content/src/workflow-guide/visual-checkpoints.txt
|
|
2429
|
-
var visual_checkpoints_default = "Visual checkpoints capture a meaningful screen state and compare it with the\nrolling baseline for that checkpoint. Add one checkpoint at each state whose\nappearance matters to the user; avoid taking several screenshots of the same\nstate just because the journey has several steps.\n\nNATIVE WORKFLOW CHECKPOINTS\n\n Checkpoints run in Sandboxes v2. Seed the sandbox into the exact state to\n compare, then capture it. A standalone visual regression test is a short\n workflow that prepares one state and takes its checkpoint. Examples include\n seeding a populated list before capturing it, or submitting invalid input and\n capturing the resulting validation error.\n\n The `target` is optional plain English describing an element, such as\n `the invoices table`; omit it to capture the whole page. Never put a CSS,\n XPath, or Playwright selector in `target`.\n\n `expected` is optional plain language describing the correct visible state.\n It guides the AI in deciding whether the current state is correct and whether\n a visual change is real or can be healed. Use a specific observable\n description, for example, \"Three invoices are listed, each marked Paid, with\n no error banner.\"\n\n `record` is the default. It records visual findings without changing the\n workflow result, and compares after the suite finishes. Choose `fail` when an\n unresolved visual change should fail this workflow step; it compares inline.\n Under either severity, a `target` that cannot be found is reported as a\n checkpoint that could not be compared, and under `fail` it fails the step.\n Both severities appear in Visual results.\n\n Dynamic text is masked automatically. If the AI heals a difference, it saves\n an element mask and its reason on the checkpoint. The review shows the heal;\n a reviewer can revert the mask or mark the change as a regression. Masks are\n automatic or healed; never author mask rules yourself.\n\nSTANDALONE VISUAL REGRESSION SPECS\n\n These complete `.cofactor.md` examples use the current spec grammar. Replace\n the illustrative seed name, persona, and routes with the published seed,\n persona, and paths used by your application. In each, prepare one meaningful\n state and capture it. Sandboxes v2 supplies the run.\n\n Seed a populated invoice list, then checkpoint it:\n\n ---\n name: Invoice list visual regression\n actor: admin\n authenticated: true\n ---\n\n ## Spirit\n > An admin can see the seeded invoices and their payment status.\n\n ## Journey\n\n **Seed** invoice-list-state\n\n **Navigate to** `/invoices`\n\n **Visual checkpoint \u2014 Populated invoice list**\n - target: the invoices table\n - expected: Three invoices are listed, each marked Paid, with no error banner.\n - severity: fail\n\n **End**.\n\n Submit invalid form data and checkpoint the validation state:\n\n ---\n name: Signup validation visual regression\n authenticated: false\n ---\n\n ## Spirit\n > A visitor sees clear validation feedback when required signup fields are missing.\n\n ## Journey\n\n **Navigate to** `/signup`\n\n **Action \u2014 Submit the empty signup form**\n > Submit without entering any values so the required-field messages appear.\n - assert (fail): A required-field validation message is visible.\n\n **Visual checkpoint \u2014 Required-field validation**\n - target: the signup form\n - expected: Required fields have clear validation messages and the form has not been submitted.\n - severity: record\n\n **End**.\n\nPLAYWRIGHT ATTACHMENT CHECKPOINTS\n\n In an external Playwright suite running on Sandboxes v2, an attached PNG is a\n visual checkpoint:\n\n await testInfo.attach(name, { body: await page.screenshot({ mask }), contentType: 'image/png' })\n\n Its identity is the spec file, test title, and attachment name. Keep masking\n in the test code with Playwright's `mask` option. The service explains and\n reports Playwright changes but never fails the Playwright test. Failure\n screenshots, trace frames, and `toHaveScreenshot` actuals are excluded. Do\n not use `toHaveScreenshot` for this feature; attach the screenshot instead.\n\nREVIEW RESULTS\n\n Review changes and comparisons on the Visual page. From the CLI, use\n `cofactor visual list` to find captures. Mark or clear a regression with\n `cofactor visual regression <captureId>` or\n `cofactor visual regression <captureId> --clear`.\n";
|
|
2443
|
+
var visual_checkpoints_default = "Visual checkpoints capture a meaningful screen state and compare it with the\nrolling baseline for that checkpoint. Add one checkpoint at each state whose\nappearance matters to the user; avoid taking several screenshots of the same\nstate just because the journey has several steps.\n\nA pull request that changes how a screen looks needs one of these as evidence:\nsee `cofactor proofs guide`.\n\nNATIVE WORKFLOW CHECKPOINTS\n\n Checkpoints run in Sandboxes v2. Seed the sandbox into the exact state to\n compare, then capture it. A standalone visual regression test is a short\n workflow that prepares one state and takes its checkpoint. Examples include\n seeding a populated list before capturing it, or submitting invalid input and\n capturing the resulting validation error.\n\n The `target` is optional plain English describing an element, such as\n `the invoices table`; omit it to capture the whole page. Never put a CSS,\n XPath, or Playwright selector in `target`.\n\n `expected` is optional plain language describing the correct visible state.\n It guides the AI in deciding whether the current state is correct and whether\n a visual change is real or can be healed. Use a specific observable\n description, for example, \"Three invoices are listed, each marked Paid, with\n no error banner.\"\n\n `record` is the default. It records visual findings without changing the\n workflow result, and compares after the suite finishes. Choose `fail` when an\n unresolved visual change should fail this workflow step; it compares inline.\n Under either severity, a `target` that cannot be found is reported as a\n checkpoint that could not be compared, and under `fail` it fails the step.\n Both severities appear in Visual results.\n\n Dynamic text is masked automatically. If the AI heals a difference, it saves\n an element mask and its reason on the checkpoint. The review shows the heal;\n a reviewer can revert the mask or mark the change as a regression. Masks are\n automatic or healed; never author mask rules yourself.\n\nSTANDALONE VISUAL REGRESSION SPECS\n\n These complete `.cofactor.md` examples use the current spec grammar. Replace\n the illustrative seed name, persona, and routes with the published seed,\n persona, and paths used by your application. In each, prepare one meaningful\n state and capture it. Sandboxes v2 supplies the run.\n\n Seed a populated invoice list, then checkpoint it:\n\n ---\n name: Invoice list visual regression\n actor: admin\n authenticated: true\n ---\n\n ## Spirit\n > An admin can see the seeded invoices and their payment status.\n\n ## Journey\n\n **Seed** invoice-list-state\n\n **Navigate to** `/invoices`\n\n **Visual checkpoint \u2014 Populated invoice list**\n - target: the invoices table\n - expected: Three invoices are listed, each marked Paid, with no error banner.\n - severity: fail\n\n **End**.\n\n Submit invalid form data and checkpoint the validation state:\n\n ---\n name: Signup validation visual regression\n authenticated: false\n ---\n\n ## Spirit\n > A visitor sees clear validation feedback when required signup fields are missing.\n\n ## Journey\n\n **Navigate to** `/signup`\n\n **Action \u2014 Submit the empty signup form**\n > Submit without entering any values so the required-field messages appear.\n - assert (fail): A required-field validation message is visible.\n\n **Visual checkpoint \u2014 Required-field validation**\n - target: the signup form\n - expected: Required fields have clear validation messages and the form has not been submitted.\n - severity: record\n\n **End**.\n\nPLAYWRIGHT ATTACHMENT CHECKPOINTS\n\n In an external Playwright suite running on Sandboxes v2, an attached PNG is a\n visual checkpoint:\n\n await testInfo.attach(name, { body: await page.screenshot({ mask }), contentType: 'image/png' })\n\n Its identity is the spec file, test title, and attachment name. Keep masking\n in the test code with Playwright's `mask` option. The service explains and\n reports Playwright changes but never fails the Playwright test. Failure\n screenshots, trace frames, and `toHaveScreenshot` actuals are excluded. Do\n not use `toHaveScreenshot` for this feature; attach the screenshot instead.\n\nREVIEW RESULTS\n\n Review changes and comparisons on the Visual page. From the CLI, use\n `cofactor visual list` to find captures. Mark or clear a regression with\n `cofactor visual regression <captureId>` or\n `cofactor visual regression <captureId> --clear`.\n";
|
|
2430
2444
|
|
|
2431
2445
|
// ../content/src/workflow-guide/index.ts
|
|
2432
2446
|
var WORKFLOW_GUIDE_CONCEPT_SECTIONS = [
|
|
@@ -2447,6 +2461,7 @@ var WORKFLOW_GUIDE_CONCEPT_SECTIONS = [
|
|
|
2447
2461
|
var WORKFLOW_GUIDE_CLI_SECTIONS = [
|
|
2448
2462
|
{ title: "CLI: Seed Workflows", slug: "cli-seeds", content: cli_seeds_default },
|
|
2449
2463
|
{ title: "CLI: API Sequences", slug: "cli-api-sequences", content: cli_api_sequences_default },
|
|
2464
|
+
{ title: "Triage Test Results", slug: "triage-results", content: triage_results_default },
|
|
2450
2465
|
{ title: "Write a Test from the CLI", slug: "write-a-test", content: write_a_test_default }
|
|
2451
2466
|
];
|
|
2452
2467
|
var WORKFLOW_GUIDE_SECTIONS = [
|
|
@@ -2460,6 +2475,7 @@ var GUIDES = {
|
|
|
2460
2475
|
sandbox: SANDBOX_GUIDE_SECTIONS,
|
|
2461
2476
|
"sandbox-v2": SANDBOX_V2_GUIDE_SECTIONS,
|
|
2462
2477
|
"product-map": PRODUCT_MAP_GUIDE_SECTIONS,
|
|
2478
|
+
"exploratory-test": EXPLORATORY_TEST_GUIDE_SECTIONS,
|
|
2463
2479
|
"external-run": EXTERNAL_RUN_GUIDE_SECTIONS,
|
|
2464
2480
|
issues: ISSUES_GUIDE_SECTIONS,
|
|
2465
2481
|
session: SESSION_GUIDE_SECTIONS,
|
|
@@ -2545,4 +2561,4 @@ export {
|
|
|
2545
2561
|
SANDBOX_V2_GUIDE_SECTIONS,
|
|
2546
2562
|
printServedGuide
|
|
2547
2563
|
};
|
|
2548
|
-
//# sourceMappingURL=chunk-
|
|
2564
|
+
//# sourceMappingURL=chunk-QHRWXCON.js.map
|