@runcofactor/cli 0.52.0 → 0.53.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 -0
- package/dist/{account-3ZG24MOE.js → account-MRZ54P5P.js} +5 -5
- package/dist/{alert-5G7PTAX2.js → alert-XBHELHEI.js} +8 -8
- package/dist/{api-catalog-IBYAFKKQ.js → api-catalog-GWYRX53L.js} +5 -5
- package/dist/{api-key-UXC44BDR.js → api-key-FH6JN447.js} +5 -5
- package/dist/{attachment-T6IRFQW3.js → attachment-J7GRVT5F.js} +5 -5
- package/dist/{auth-SHOCJE6Z.js → auth-VZZQXRDR.js} +3 -3
- package/dist/{benchmark-NJ2U6VTC.js → benchmark-A3SNDARF.js} +5 -5
- package/dist/{blackcat-RQJVFGXQ.js → blackcat-UM6TBQBN.js} +3 -3
- package/dist/{chunk-DHAGRM2F.js → chunk-33U56YGD.js} +14 -5
- package/dist/{chunk-DHAGRM2F.js.map → chunk-33U56YGD.js.map} +1 -1
- package/dist/{chunk-M752EUK3.js → chunk-3JU4UYYN.js} +5 -5
- package/dist/{chunk-G6BCST3G.js → chunk-3ZOUNCA5.js} +7 -7
- package/dist/{chunk-76Z6E7XX.js → chunk-4UZN6TVT.js} +74 -131
- package/dist/chunk-4UZN6TVT.js.map +1 -0
- package/dist/{chunk-ABT3BLRC.js → chunk-5TKXHS3X.js} +3 -3
- package/dist/{chunk-QCYSONXI.js → chunk-CRYHWO4M.js} +2 -2
- package/dist/{chunk-QTYUM4ZB.js → chunk-EJ3ZPH5U.js} +23 -187
- package/dist/chunk-EJ3ZPH5U.js.map +1 -0
- package/dist/{chunk-I5RHTCPY.js → chunk-EL6KJ4PO.js} +4 -4
- package/dist/{chunk-ZL4O6C42.js → chunk-HXBK3AGD.js} +2 -2
- package/dist/{chunk-PRERH4DN.js → chunk-I2SOHQ2C.js} +3 -3
- package/dist/{chunk-CYSPCGO6.js → chunk-JEHHEMUL.js} +5 -5
- package/dist/{chunk-LIEADTEF.js → chunk-LJFVRJCA.js} +2 -2
- package/dist/{chunk-UEJ2HTKU.js → chunk-MJVUMVLO.js} +19 -1
- package/dist/chunk-MJVUMVLO.js.map +1 -0
- package/dist/chunk-N55RKCGR.js +24 -0
- package/dist/chunk-N55RKCGR.js.map +1 -0
- package/dist/{chunk-PHNH3CEU.js → chunk-O4Z44BR7.js} +5 -5
- package/dist/{chunk-QA37WVF6.js → chunk-OQFCZERZ.js} +6 -6
- package/dist/{chunk-EUVB6Y3C.js → chunk-P7L5KELH.js} +8 -8
- package/dist/chunk-P7L5KELH.js.map +1 -0
- package/dist/{chunk-Z3MDQSZG.js → chunk-PJFFGQSB.js} +2 -2
- package/dist/{chunk-IY3ZK46I.js → chunk-QOVJUSSL.js} +2 -2
- package/dist/{chunk-RPJMQBB5.js → chunk-QXU4G4YN.js} +3 -3
- package/dist/{chunk-SZSTJIYU.js → chunk-RRSUKARV.js} +4 -4
- package/dist/{chunk-775FI436.js → chunk-RVD6J6IZ.js} +2 -2
- package/dist/{chunk-5UUIBY6E.js → chunk-TAVBFLCR.js} +3 -3
- package/dist/{chunk-ROX7N3RK.js → chunk-V7HB5LAU.js} +10 -7
- package/dist/chunk-V7HB5LAU.js.map +1 -0
- package/dist/{chunk-LL6TSQWV.js → chunk-VKZOHJCE.js} +3 -3
- package/dist/chunk-VKZOHJCE.js.map +1 -0
- package/dist/{chunk-3HI6QC4V.js → chunk-WRBU6TX7.js} +2 -2
- package/dist/{proofs-TXJGAP5F.js → chunk-X7SUSIY3.js} +118 -20
- package/dist/chunk-X7SUSIY3.js.map +1 -0
- package/dist/{chunk-BF3IPGMP.js → chunk-XSSZMG65.js} +4 -4
- package/dist/{credential-TWSWV64F.js → credential-4RFRU6OH.js} +5 -5
- package/dist/{credential-template-XO5D5AI3.js → credential-template-36PFUTBN.js} +5 -5
- package/dist/{digest-SLEFOLPV.js → digest-THMKFMP7.js} +8 -8
- package/dist/{docs-IDC4V4WQ.js → docs-ZGM53L67.js} +5 -5
- package/dist/{doctor-SDAYEA6C.js → doctor-Z2XBGBM3.js} +3 -3
- package/dist/{environment-VBUIBQ6O.js → environment-5YVZXBN7.js} +7 -7
- package/dist/eval-SO5TPD37.js +1177 -0
- package/dist/eval-SO5TPD37.js.map +1 -0
- package/dist/{exploration-IREIR5PO.js → exploration-GZ4WUJJH.js} +5 -5
- package/dist/{exploratory-test-4Q7BMEFT.js → exploratory-test-Y7P4ZDLD.js} +5 -5
- package/dist/{external-run-S7LULKPT.js → external-run-IJGGZOGI.js} +7 -7
- package/dist/{feature-flag-IASHTUGU.js → feature-flag-UH6Z7774.js} +5 -5
- package/dist/{flow-IBEZFZTU.js → flow-MRWAQYZZ.js} +5 -5
- package/dist/{github-HAEZRULP.js → github-VDJU3PU3.js} +6 -6
- package/dist/index.js +101 -82
- package/dist/index.js.map +1 -1
- package/dist/{init-LT2TZMQY.js → init-CROSTPA6.js} +5 -5
- package/dist/{install-RTEYH6TK.js → install-ECR7OLAY.js} +5 -5
- package/dist/{issues-5GE277R2.js → issues-K5TL2S4Z.js} +7 -7
- package/dist/{knobs-AMIWGYUZ.js → knobs-OKQFBQLK.js} +5 -5
- package/dist/{landing-3VLVVQL5.js → landing-XANIBPVM.js} +7 -7
- package/dist/{list-GDJR6UY6.js → list-64GX47Q4.js} +5 -5
- package/dist/{local-4R6GVW7U.js → local-QMQY2ZUX.js} +7 -7
- package/dist/{login-IXVAWTLO.js → login-I7UQ3GTM.js} +5 -5
- package/dist/{mcp-BM2MJWX4.js → mcp-XZV63DX5.js} +11 -11
- package/dist/{mcp-BM2MJWX4.js.map → mcp-XZV63DX5.js.map} +1 -1
- package/dist/{metrics-SSIP5CEB.js → metrics-KBTQ3KGZ.js} +5 -5
- package/dist/{monitor-ESZYHXT2.js → monitor-UGWY43IU.js} +5 -5
- package/dist/perf-CCHWHCSR.js +961 -0
- package/dist/perf-CCHWHCSR.js.map +1 -0
- package/dist/{product-VCGUV5TH.js → product-KLG2ZTHC.js} +5 -5
- package/dist/{product-map-AXKZNQDM.js → product-map-AMEZ76EQ.js} +5 -5
- package/dist/{prompts-FVPSON2N.js → prompts-ZVQNGL7X.js} +5 -5
- package/dist/proofs-QCNSQAPX.js +20 -0
- package/dist/{property-XEPCC7KQ.js → property-PJDQFHAL.js} +6 -6
- package/dist/{psql-JKF3OYYJ.js → psql-AURAN27C.js} +5 -5
- package/dist/{record-REQ37S54.js → record-SYWBTUOB.js} +6 -6
- package/dist/{redis-WGHT2GJB.js → redis-2H752SKO.js} +5 -5
- package/dist/{release-SNDIVIA6.js → release-OSIYUXSC.js} +5 -5
- package/dist/{runbook-XFJG2LFC.js → runbook-7LVHF6KV.js} +5 -5
- package/dist/{sandbox-DYCKLCMY.js → sandbox-TJQPMRGS.js} +18 -30
- package/dist/sandbox-TJQPMRGS.js.map +1 -0
- package/dist/{scenarios-PVHCKP55.js → scenarios-P2ABU4HK.js} +5 -5
- package/dist/{schedule-DGC7CX23.js → schedule-XLS56NP5.js} +8 -8
- package/dist/{session-ERGUFDDI.js → session-ACSFC3UB.js} +8 -8
- package/dist/{skill-6QO2E3TB.js → skill-F7I5P6AI.js} +43 -28
- package/dist/{skill-6QO2E3TB.js.map → skill-F7I5P6AI.js.map} +1 -1
- package/dist/{slack-WE7THLLH.js → slack-ZEK4GRND.js} +6 -6
- package/dist/{spec-SEOY7N3L.js → spec-XPIRRSV6.js} +5 -5
- package/dist/{src-CCK3TKBX.js → src-N7ZCYCXU.js} +4 -4
- package/dist/{src-AEWV5HYH.js → src-XSBBMVGW.js} +3 -3
- package/dist/{start-EPGLHTND.js → start-DKWLOQI6.js} +5 -5
- package/dist/{status-DTE2GXM2.js → status-K7KH5DI3.js} +5 -5
- package/dist/{tag-GIFEWGBL.js → tag-2Y3OMQQC.js} +6 -6
- package/dist/{task-VLJCHSFB.js → task-4PIOC4QV.js} +6 -6
- package/dist/{teams-J2OOJBL4.js → teams-KUM56BE2.js} +83 -47
- package/dist/teams-KUM56BE2.js.map +1 -0
- package/dist/{test-suite-CTACGIAZ.js → test-suite-SW25YGJV.js} +138 -96
- package/dist/test-suite-SW25YGJV.js.map +1 -0
- package/dist/{trace-RWQDYL4K.js → trace-QPXHYBLA.js} +7 -7
- package/dist/{triage-6UAXLCGU.js → triage-EZABRZZM.js} +6 -6
- package/dist/trigger-7C7KX5KH.js +137 -0
- package/dist/trigger-7C7KX5KH.js.map +1 -0
- package/dist/{update-check-QRREAM3N.js → update-check-E3X7UIY6.js} +3 -3
- package/dist/{upgrade-5Y655TQ6.js → upgrade-EJGTMW44.js} +4 -4
- package/dist/{usage-KCXFY46V.js → usage-GZCQDULF.js} +7 -7
- package/dist/{utilities-SUON2KZK.js → utilities-RIITD53A.js} +6 -6
- package/dist/{visual-GJZVJC32.js → visual-FH67OKS2.js} +5 -5
- package/dist/{workflow-R5QAXWBX.js → workflow-34YJZ2QQ.js} +38 -86
- package/dist/workflow-34YJZ2QQ.js.map +1 -0
- package/dist/{workflow-cache-PFBTWLGM.js → workflow-cache-YQ3BGJIG.js} +17 -17
- package/dist/{workflow-cache-convergence-resolve-UZGRPC3B.js → workflow-cache-convergence-resolve-SPLBRSNK.js} +5 -5
- package/dist/{workflow-debug-RACNWWOL.js → workflow-debug-HZB7TJGK.js} +10 -10
- package/dist/workflow-debug-HZB7TJGK.js.map +1 -0
- package/dist/{workflow-feedback-RJIQ5R3R.js → workflow-feedback-6UOTIWIQ.js} +5 -5
- package/dist/{workflow-fork-LINDCYJ3.js → workflow-fork-IDYHAHCX.js} +5 -5
- package/dist/workflow-promotion-U6WYFY5U.js +62 -0
- package/dist/workflow-promotion-U6WYFY5U.js.map +1 -0
- package/dist/{workflow-proposals-E4EXKAGM.js → workflow-proposals-MMVNIQT3.js} +5 -5
- package/dist/{workflow-revisions-5M6APN6R.js → workflow-revisions-BPUOLCZB.js} +5 -5
- package/dist/{workflow-status-7KL73WS2.js → workflow-status-RA3IEMEW.js} +5 -5
- package/package.json +3 -2
- package/dist/chat-target-IZ7YLLU3.js +0 -502
- package/dist/chat-target-IZ7YLLU3.js.map +0 -1
- package/dist/chunk-76Z6E7XX.js.map +0 -1
- package/dist/chunk-EUVB6Y3C.js.map +0 -1
- package/dist/chunk-LL6TSQWV.js.map +0 -1
- package/dist/chunk-QTYUM4ZB.js.map +0 -1
- package/dist/chunk-ROX7N3RK.js.map +0 -1
- package/dist/chunk-UEJ2HTKU.js.map +0 -1
- package/dist/proofs-TXJGAP5F.js.map +0 -1
- package/dist/sandbox-DYCKLCMY.js.map +0 -1
- package/dist/teams-J2OOJBL4.js.map +0 -1
- package/dist/test-suite-CTACGIAZ.js.map +0 -1
- package/dist/workflow-R5QAXWBX.js.map +0 -1
- /package/dist/{account-3ZG24MOE.js.map → account-MRZ54P5P.js.map} +0 -0
- /package/dist/{alert-5G7PTAX2.js.map → alert-XBHELHEI.js.map} +0 -0
- /package/dist/{api-catalog-IBYAFKKQ.js.map → api-catalog-GWYRX53L.js.map} +0 -0
- /package/dist/{api-key-UXC44BDR.js.map → api-key-FH6JN447.js.map} +0 -0
- /package/dist/{attachment-T6IRFQW3.js.map → attachment-J7GRVT5F.js.map} +0 -0
- /package/dist/{auth-SHOCJE6Z.js.map → auth-VZZQXRDR.js.map} +0 -0
- /package/dist/{benchmark-NJ2U6VTC.js.map → benchmark-A3SNDARF.js.map} +0 -0
- /package/dist/{blackcat-RQJVFGXQ.js.map → blackcat-UM6TBQBN.js.map} +0 -0
- /package/dist/{chunk-M752EUK3.js.map → chunk-3JU4UYYN.js.map} +0 -0
- /package/dist/{chunk-G6BCST3G.js.map → chunk-3ZOUNCA5.js.map} +0 -0
- /package/dist/{chunk-ABT3BLRC.js.map → chunk-5TKXHS3X.js.map} +0 -0
- /package/dist/{chunk-QCYSONXI.js.map → chunk-CRYHWO4M.js.map} +0 -0
- /package/dist/{chunk-I5RHTCPY.js.map → chunk-EL6KJ4PO.js.map} +0 -0
- /package/dist/{chunk-ZL4O6C42.js.map → chunk-HXBK3AGD.js.map} +0 -0
- /package/dist/{chunk-PRERH4DN.js.map → chunk-I2SOHQ2C.js.map} +0 -0
- /package/dist/{chunk-CYSPCGO6.js.map → chunk-JEHHEMUL.js.map} +0 -0
- /package/dist/{chunk-LIEADTEF.js.map → chunk-LJFVRJCA.js.map} +0 -0
- /package/dist/{chunk-PHNH3CEU.js.map → chunk-O4Z44BR7.js.map} +0 -0
- /package/dist/{chunk-QA37WVF6.js.map → chunk-OQFCZERZ.js.map} +0 -0
- /package/dist/{chunk-Z3MDQSZG.js.map → chunk-PJFFGQSB.js.map} +0 -0
- /package/dist/{chunk-IY3ZK46I.js.map → chunk-QOVJUSSL.js.map} +0 -0
- /package/dist/{chunk-RPJMQBB5.js.map → chunk-QXU4G4YN.js.map} +0 -0
- /package/dist/{chunk-SZSTJIYU.js.map → chunk-RRSUKARV.js.map} +0 -0
- /package/dist/{chunk-775FI436.js.map → chunk-RVD6J6IZ.js.map} +0 -0
- /package/dist/{chunk-5UUIBY6E.js.map → chunk-TAVBFLCR.js.map} +0 -0
- /package/dist/{chunk-3HI6QC4V.js.map → chunk-WRBU6TX7.js.map} +0 -0
- /package/dist/{chunk-BF3IPGMP.js.map → chunk-XSSZMG65.js.map} +0 -0
- /package/dist/{credential-TWSWV64F.js.map → credential-4RFRU6OH.js.map} +0 -0
- /package/dist/{credential-template-XO5D5AI3.js.map → credential-template-36PFUTBN.js.map} +0 -0
- /package/dist/{digest-SLEFOLPV.js.map → digest-THMKFMP7.js.map} +0 -0
- /package/dist/{docs-IDC4V4WQ.js.map → docs-ZGM53L67.js.map} +0 -0
- /package/dist/{doctor-SDAYEA6C.js.map → doctor-Z2XBGBM3.js.map} +0 -0
- /package/dist/{environment-VBUIBQ6O.js.map → environment-5YVZXBN7.js.map} +0 -0
- /package/dist/{exploration-IREIR5PO.js.map → exploration-GZ4WUJJH.js.map} +0 -0
- /package/dist/{exploratory-test-4Q7BMEFT.js.map → exploratory-test-Y7P4ZDLD.js.map} +0 -0
- /package/dist/{external-run-S7LULKPT.js.map → external-run-IJGGZOGI.js.map} +0 -0
- /package/dist/{feature-flag-IASHTUGU.js.map → feature-flag-UH6Z7774.js.map} +0 -0
- /package/dist/{flow-IBEZFZTU.js.map → flow-MRWAQYZZ.js.map} +0 -0
- /package/dist/{github-HAEZRULP.js.map → github-VDJU3PU3.js.map} +0 -0
- /package/dist/{init-LT2TZMQY.js.map → init-CROSTPA6.js.map} +0 -0
- /package/dist/{install-RTEYH6TK.js.map → install-ECR7OLAY.js.map} +0 -0
- /package/dist/{issues-5GE277R2.js.map → issues-K5TL2S4Z.js.map} +0 -0
- /package/dist/{knobs-AMIWGYUZ.js.map → knobs-OKQFBQLK.js.map} +0 -0
- /package/dist/{landing-3VLVVQL5.js.map → landing-XANIBPVM.js.map} +0 -0
- /package/dist/{list-GDJR6UY6.js.map → list-64GX47Q4.js.map} +0 -0
- /package/dist/{local-4R6GVW7U.js.map → local-QMQY2ZUX.js.map} +0 -0
- /package/dist/{login-IXVAWTLO.js.map → login-I7UQ3GTM.js.map} +0 -0
- /package/dist/{metrics-SSIP5CEB.js.map → metrics-KBTQ3KGZ.js.map} +0 -0
- /package/dist/{monitor-ESZYHXT2.js.map → monitor-UGWY43IU.js.map} +0 -0
- /package/dist/{product-VCGUV5TH.js.map → product-KLG2ZTHC.js.map} +0 -0
- /package/dist/{product-map-AXKZNQDM.js.map → product-map-AMEZ76EQ.js.map} +0 -0
- /package/dist/{prompts-FVPSON2N.js.map → prompts-ZVQNGL7X.js.map} +0 -0
- /package/dist/{src-AEWV5HYH.js.map → proofs-QCNSQAPX.js.map} +0 -0
- /package/dist/{property-XEPCC7KQ.js.map → property-PJDQFHAL.js.map} +0 -0
- /package/dist/{psql-JKF3OYYJ.js.map → psql-AURAN27C.js.map} +0 -0
- /package/dist/{record-REQ37S54.js.map → record-SYWBTUOB.js.map} +0 -0
- /package/dist/{redis-WGHT2GJB.js.map → redis-2H752SKO.js.map} +0 -0
- /package/dist/{release-SNDIVIA6.js.map → release-OSIYUXSC.js.map} +0 -0
- /package/dist/{runbook-XFJG2LFC.js.map → runbook-7LVHF6KV.js.map} +0 -0
- /package/dist/{scenarios-PVHCKP55.js.map → scenarios-P2ABU4HK.js.map} +0 -0
- /package/dist/{schedule-DGC7CX23.js.map → schedule-XLS56NP5.js.map} +0 -0
- /package/dist/{session-ERGUFDDI.js.map → session-ACSFC3UB.js.map} +0 -0
- /package/dist/{slack-WE7THLLH.js.map → slack-ZEK4GRND.js.map} +0 -0
- /package/dist/{spec-SEOY7N3L.js.map → spec-XPIRRSV6.js.map} +0 -0
- /package/dist/{src-CCK3TKBX.js.map → src-N7ZCYCXU.js.map} +0 -0
- /package/dist/{tag-GIFEWGBL.js.map → src-XSBBMVGW.js.map} +0 -0
- /package/dist/{start-EPGLHTND.js.map → start-DKWLOQI6.js.map} +0 -0
- /package/dist/{status-DTE2GXM2.js.map → status-K7KH5DI3.js.map} +0 -0
- /package/dist/{trace-RWQDYL4K.js.map → tag-2Y3OMQQC.js.map} +0 -0
- /package/dist/{task-VLJCHSFB.js.map → task-4PIOC4QV.js.map} +0 -0
- /package/dist/{triage-6UAXLCGU.js.map → trace-QPXHYBLA.js.map} +0 -0
- /package/dist/{update-check-QRREAM3N.js.map → triage-EZABRZZM.js.map} +0 -0
- /package/dist/{upgrade-5Y655TQ6.js.map → update-check-E3X7UIY6.js.map} +0 -0
- /package/dist/{utilities-SUON2KZK.js.map → upgrade-EJGTMW44.js.map} +0 -0
- /package/dist/{usage-KCXFY46V.js.map → usage-GZCQDULF.js.map} +0 -0
- /package/dist/{workflow-debug-RACNWWOL.js.map → utilities-RIITD53A.js.map} +0 -0
- /package/dist/{visual-GJZVJC32.js.map → visual-FH67OKS2.js.map} +0 -0
- /package/dist/{workflow-cache-PFBTWLGM.js.map → workflow-cache-YQ3BGJIG.js.map} +0 -0
- /package/dist/{workflow-cache-convergence-resolve-UZGRPC3B.js.map → workflow-cache-convergence-resolve-SPLBRSNK.js.map} +0 -0
- /package/dist/{workflow-feedback-RJIQ5R3R.js.map → workflow-feedback-6UOTIWIQ.js.map} +0 -0
- /package/dist/{workflow-fork-LINDCYJ3.js.map → workflow-fork-IDYHAHCX.js.map} +0 -0
- /package/dist/{workflow-proposals-E4EXKAGM.js.map → workflow-proposals-MMVNIQT3.js.map} +0 -0
- /package/dist/{workflow-revisions-5M6APN6R.js.map → workflow-revisions-BPUOLCZB.js.map} +0 -0
- /package/dist/{workflow-status-7KL73WS2.js.map → workflow-status-RA3IEMEW.js.map} +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
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
|
parseDurationSeconds
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XSSZMG65.js";
|
|
5
5
|
import {
|
|
6
6
|
resolveApplicationId,
|
|
7
7
|
resolveOrCreateSandboxApplication
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-JEHHEMUL.js";
|
|
9
|
+
import "./chunk-RVD6J6IZ.js";
|
|
10
10
|
import {
|
|
11
11
|
findOperation,
|
|
12
12
|
getAllPlugins,
|
|
13
13
|
getPlugin,
|
|
14
14
|
getPluginIds,
|
|
15
15
|
pluginOperations
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-33U56YGD.js";
|
|
17
17
|
import {
|
|
18
18
|
UUID_RE,
|
|
19
19
|
apiRequest,
|
|
@@ -22,14 +22,14 @@ import {
|
|
|
22
22
|
getTargetOrg,
|
|
23
23
|
selectProperty,
|
|
24
24
|
stripOneTrailingNewline
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-VKZOHJCE.js";
|
|
26
26
|
import "./chunk-S3Q7MDCS.js";
|
|
27
27
|
import {
|
|
28
28
|
INSTANCE_SIZES,
|
|
29
29
|
INSTANCE_SIZE_SPEC,
|
|
30
30
|
SANDBOX_EGRESS_MODES,
|
|
31
31
|
parseInstanceSize
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-4UZN6TVT.js";
|
|
33
33
|
import {
|
|
34
34
|
apiFetch,
|
|
35
35
|
decodeJwtPayload,
|
|
@@ -39,12 +39,12 @@ import {
|
|
|
39
39
|
isTerminalFailure,
|
|
40
40
|
resolveConfig,
|
|
41
41
|
splitAtSeparator
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-WRBU6TX7.js";
|
|
43
43
|
import {
|
|
44
44
|
SANDBOX_GUIDE_SECTIONS,
|
|
45
45
|
printGuide,
|
|
46
46
|
writeRawOutput
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-MJVUMVLO.js";
|
|
48
48
|
import "./chunk-TOWZAG3I.js";
|
|
49
49
|
|
|
50
50
|
// src/sandbox.ts
|
|
@@ -55,7 +55,7 @@ import { readFileSync as readFileSync6 } from "fs";
|
|
|
55
55
|
var overview_default = "A Sandboxes v2 sandbox is a VM restored from a stack of snapshot layers,\nnot a container stood up from scratch each time. That's what makes claims\nfast: the work (installing the toolchain and dependencies, building the\napp, seeding its data) happens once per commit and environment and is\nbaked into a snapshot, and every claim restores its own copy.\n\nEvery example in this guide uses one invented app: a project-tracking\nSaaS with workspaces, their members (roles: owner, member, viewer),\nprojects and tasks, a REST API with cookie sessions, and a Postgres\ndatabase. Substitute your own app's objects, roles and endpoints.\n\nLAYERS\n\n platform Cofactor's sandbox helper, which runs your services,\n prepare commands and actions. Rebuilt only when the\n helper's contract with Cofactor changes.\n\n toolchain `setup` from cofactor.sandbox.yaml, plus `resources` and the\n declared `ports`. Rebuilt only when those change \u2014 this is\n the expensive layer (apt packages, language runtimes,\n database clusters).\n\n repo base The repository checked out and built at a commit:\n `lifecycle.build` steps, each rerun only when the git\n pathspecs in its `inputs` changed since the layer this one\n is stacked on. This is what makes \"the same commit, again\"\n nearly free.\n\n ready One commit and one environment: the repo base moved to the\n snapshot commit, prepared, and the environment's seeds run \u2014 then\n captured while its services are running. Built once, the\n first time anything asks for that commit and environment;\n every claim is a copy of it.\n\nA GitHub-delivered sandbox is identified by **repository + definition\npath**, not by repository alone, so a monorepo can hold several (see\nbelow). Pushed images have no repository or build layers: the image is\nthe base, and each push gets its own ready snapshots (see the `upload`\ntopic).\n\nTHE CLAIM LIFECYCLE\n\n 1. snapshot The claim needs the ready snapshot for its commit and\n environment. If there isn't one yet, it is built first\n (once, however many claims are waiting on it):\n - the nearest layer (toolchain or repo base) is\n restored;\n - the outstanding `lifecycle.build` steps bring it to\n the commit (only the steps whose `inputs` changed);\n - `prepare` runs (see step 3);\n - the environment's seed chain runs, in order (parent's\n seeds before its own \u2014 see the `environments` topic);\n each step is an action or an emulator operation,\n and its output can be referenced by later steps;\n - the VM is captured with its services running.\n 2. restore A copy of the ready snapshot is restored. Nothing is\n built or seeded: the copy starts exactly where the\n snapshot left off, data included.\n 3. identity A copy whose port params are all live, with stored snapshot\n prepare results, receives public port URLs through one\n copy-values exec. The sandbox materializes\n /var/lib/cofactor/live-env.json for the\n running app; lifecycle.prepare does NOT run. There is no\n service restart, emulator reconfiguration or bucket creation.\n Secrets, persona passwords, emulator token/config and seeded\n storage stay exactly as captured. A user claim receives its\n private sync key by a separate exec, never in the copy-values\n exec or its file. Non-user copies cannot be synced.\n Legacy prepare runs instead when any port param is non-live,\n an env key mixes live/non-live params, or an emulator config\n references a port param; it keeps per-copy prepare and restart.\n Pinned PR-preview captures without stored prepare results\n also keep the existing copy prepare path.\n 4. ready The sandbox reports its port URLs, persona logins, and the\n seed outputs (the same for every copy of the snapshot).\n From here it behaves like a real running instance of the\n app until it's destroyed or its lease expires. A copy is\n never reused.\n\n`cofactor sandbox show` breaks the claim's time down into these phases.\n\nA failure while building the snapshot (a build step, `prepare` or a seed)\nfails the claims waiting on it; the next claim of that commit and\nenvironment builds the snapshot again, so a failure that happens every\ntime needs a fix and a new commit or push. See the `verify` topic for what to do\nwhen that happens.\n\nA failure of the sandbox platform itself (a worker that is slow to restore,\na capture that is still running) is retried: the snapshot's build tries\nagain, resuming where it stopped, and so does claiming the copy when the\nfleet has no room. Every claim has a time limit \u2014 2 hours unless\n`cofactor sandbox claim --timeout` sets another. A claim not ready by then\nfails with the last problem it met, and a sandbox is never handed over after\nit: a copy still being made at the limit is destroyed when it finishes. That\ncan be up to about 10 minutes of waiting for room plus the copy's prepare\nafter the limit, so the failure may arrive later than the limit itself.\n\nAPPLICATIONS, AND WHERE THEIR CODE COMES FROM\n\n A sandbox is always a sandbox of a Cofactor application. How the\n application's code reaches its sandboxes is its own setting:\n\n - GitHub (`cofactor sandbox connect --application A --repo owner/name\n [--dir D]`): the `cofactor.sandbox.yaml` under `D` at each commit\n installs (`setup`) and builds (`lifecycle.build`) a checkout. A\n monorepo can connect one application per definition directory.\n Commands run **from that definition's directory** \u2014 an action's\n `run:` path, build steps and services resolve relative to where the\n `cofactor.sandbox.yaml` lives, not the repo root.\n - Pushed images (`cofactor sandbox push`), for a team that gives\n Cofactor no repository access: see the `upload` topic.\n\n Either way, each environment in the definition is one sandbox\n environment of the application, and each persona one of its personas, so\n test runs on that environment sign in with the sandbox's own logins.\n There is no need to create the application first: `connect` or `push`\n with a new application name creates it, and its sandbox `default`\n environment is its default environment.\n\nHOW A TEST AGENT USES THIS\n\n 1. Browse what's available: `cofactor sandbox envs --application <name>`\n lists the definition's environments, actions, emulator operations,\n ports and personas.\n 2. Claim one: `cofactor sandbox claim --application <name>\n [--environment <name>] --wait`. Without `--environment` you get\n `default`.\n 3. Write the test against the claimed sandbox: drive the app through its\n port URLs, call actions to set up data the test needs\n (`cofactor sandbox run <id> <action>`), and call emulator operations\n to inspect or drive third parties (`cofactor sandbox run <id>\n <plugin.op>`).\n 4. Try uncommitted changes against it: `cofactor sandbox sync <id>`\n writes your working tree into the sandbox, then `cofactor workflow\n run <flow> --sandbox <id>` runs a flow on it (see the `local` topic).\n 5. Destroy it when done: `cofactor sandbox destroy <id>`.\n\n Staff can keep an environment warm so claims start in about a second\n instead of fetching its snapshot first: see the `warm` topic.\n";
|
|
56
56
|
|
|
57
57
|
// ../content/src/sandbox-v2-guide/definition.txt
|
|
58
|
-
var definition_default = "Everything about a sandbox lives in one file: `cofactor.sandbox.yaml`, at the\nrepository root or under a definition directory for a monorepo (for pushed\nimages, wherever you keep it \u2014 see the `upload` topic). The examples below\nare fragments of one file for the guide's example app: a project tracker\nwith a `web` front end, an `api` server and Postgres.\n`cofactor sandbox validate` checks every field below.\n\n Required: `version`, `resources`, `ports`, `params`, `env`, `services`,\n `lifecycle` and `environments`; write `{}` for one you don't use (e.g.\n `params: {}`, `lifecycle: { prepare: [] }`). `personas`, `actions`, `emulation`, `object_storage`\n and `databases` may be left out.\n Names of ports, services, personas, actions, environments and plugins\n are lowercase letters, digits and `-` (up to 32 characters, starting\n with a letter); a seed step's `id` is lowercase letters, digits and `_`\n (it is used in `${<id>.<output>}`, so no `-`).\n\nversion\n Always `1`. The schema version of the file itself.\n\nresources\n The machine every sandbox of this application gets.\n resources: { vcpus: 2, memory_mib: 4096 }\n\nsetup\n (GitHub-delivered only.) A root shell script that installs the\n toolchain: packages, language runtimes, database clusters. Runs once per\n toolchain layer (keyed by this script's text plus `resources` and\n `ports` \u2014 see the `overview` topic).\n setup: |\n apt-get update -qq\n apt-get install -y -qq postgresql-16 nodejs npm jq curl >/dev/null\n\nports\n Named ports the app listens on, each becoming its own public URL. Several\n are normal \u2014 bundle web and API together so one sandbox serves both UI\n flows and API sequences. `access: public` gets a routable URL; `private`\n stays internal. At most 8 total, including the implicit `sync` port every\n sandbox has (so at most 7 of your own), the `emulation` port added\n automatically when `emulation` is declared, and `object-storage` when\n `object_storage` is \u2014 see the `ports` topic.\n ports:\n web: { port: 3000, access: public }\n api: { port: 3001, access: public }\n\nparams\n Per-sandbox values, filled in by `prepare`, referenced from `env` as\n `${params.name}`. Three shapes:\n - a plain string: a fixed value, same in every sandbox.\n - `{port: <name>}`: the sandbox's own public URL for that port.\n `{port: emulation}` is the emulator's public URL, allowed when\n `emulation` is declared (it is the `oidc` plugin's issuer).\n - `{secret: true}`: a random value made when the version's ready\n snapshot is built, so every sandbox of that version and environment\n shares it (e.g. a session-signing key); not unique per claim.\n params:\n web_url: { port: web }\n api_url: { port: api }\n session_secret: { secret: true }\n A port param may be `live: true`: read its resolved env at request/use time\n from `/var/lib/cofactor/live-env.json`, a JSON object of env-name -> value.\n A fast copy receives its URLs through one copy-values exec, which\n keeps this file current using atomic replacement;\n re-read when the file's inode or modification time changes. No private\n Cofactor package is needed by your app.\n Eligible copies with stored snapshot prepare results skip prepare and keep\n services running: lifecycle.prepare does NOT run. They retain baked secrets\n until the snapshot is rebuilt. A copy is eligible for the fast path only\n when every port param is live.\n Any non-live port param keeps the per-copy prepare and service restart.\n An env key mixing a live port param with a non-live param also requires\n prepare, as does an emulation plugin config that references a port param.\n A real third-party secret (a Stripe or OpenAI key) is not a param: see\n the `secrets` topic.\n\nenv\n Environment variables the services, prepare commands and actions run\n with. `${params.name}` is substituted with that param's value. An\n environment can add its own on top (see the `env` topic). A secret of\n the same name (see the `secrets` topic) overrides the `env` entry.\n (`${env.NAME}` is a different, narrower reference: it appears only as\n the whole `url` of a `databases` entry \u2014 see the `databases` field.)\n env:\n DATABASE_URL: postgres://app@127.0.0.1:5432/app\n PUBLIC_URL: ${params.web_url}\n API_URL: ${params.api_url}\n SESSION_SECRET: ${params.session_secret}\n\nservices\n Long-running processes. `cofactor-sync` is reserved: every sandbox runs\n it (the sync server, see the `local` topic). Restart a service by hand\n with `cofactor sandbox exec <id> -- cofactor-sandbox restart <name>`.\n services:\n db:\n run: exec sandbox/start-postgres.sh\n health: { command: pg_isready -h 127.0.0.1 }\n api:\n run: cd api && exec npm start\n health: { http: 3001, path: /health }\n after_build: true # don't start until lifecycle.build has produced its output\n reload_on_prepare: true # restarted by prepare, so it sees per-sandbox env\n web:\n run: cd web && exec npm run serve\n health: { http: 3000, path: / }\n after_build: true\n reload_on_prepare: true # reads PUBLIC_URL, API_URL and FEATURE_FLAGS\n\n `health` is either `{ command: <shell> }` (checked by exit code) or\n `{ http: <port>, path: </...> }`. `after_build` is for GitHub-delivered\n definitions only.\n\npersonas\n Named login identities declared here; each one's password is generated\n when the snapshot is built and shared by its copies (see the `personas` topic). Only `email` is declared\n in the definition.\n personas:\n owner: { email: owner@example.test }\n member: { email: member@example.test }\n viewer: { email: viewer@example.test }\n\nlifecycle\n build (GitHub-delivered only.) Ordered steps that produce the\n repo-base layer. Each has `name`, `run` (a shell command) and\n `inputs` (git pathspecs, relative to the definition's\n directory) \u2014 a step reruns only when the blobs its `inputs`\n match changed, or an earlier step ran.\n lifecycle:\n build:\n - name: install\n run: npm ci\n inputs: [package-lock.json, \"**/package.json\"]\n\n prepare Shell scripts run every time a sandbox is prepared (a snapshot\n build, a legacy copy claim or an in-place move to a new commit), after services with\n `reload_on_prepare` have restarted with per-sandbox values. Each\n param is also available as `COFACTOR_PARAM_<NAME>`.\n lifecycle:\n prepare:\n - bash sandbox/personas.sh\n\nactions\n Typed scripts \u2014 the one way data gets into a sandbox (seeds, tests,\n `cofactor sandbox run`). See the `seeds-and-actions` topic for\n what makes a good one; the field shapes are:\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, description: Who creates it }\n output:\n workspace_id: { type: string }\n timeout: 60 # seconds, defaults to 60, max 1800\n add-member:\n description: Adds a persona to a workspace with a role.\n run: sandbox/actions/add-member.sh\n input:\n workspace_id: { type: string, required: true }\n persona: { type: persona, required: true, description: Who is added }\n role: { type: string, required: true, enum: [owner, member, viewer] }\n as: { type: persona, required: true, description: Who adds them }\n\n Input properties: `type` (string | number | integer | boolean |\n persona \u2014 a persona's name, checked against `personas`), `description`,\n `required`, `default`, `enum`, `sensitive` (masked wherever input is\n logged or returned).\n Output properties: `type`, `description`.\n\nemulation\n Plugin id \u2192 its config (often `{}`). Declaring any plugin here adds the\n implicit, public, port-80 `emulation` port automatically \u2014 you don't\n declare it yourself. See the `emulation` topic for what each plugin\n intercepts. Plugin ids: `aws`, `email`, `google-oauth`, `jira`, `slack`, `oidc`\n (a standard OIDC provider for the app to sign in against; see the\n `authentication` topic).\n emulation:\n email: {}\n\nobject_storage\n An emulated S3 or GCS server with these buckets in every sandbox, its\n standard client env given to everything that runs, and an implicit\n public `object-storage` port. See the `object-storage` topic.\n object_storage: { provider: s3, buckets: [attachments] }\n\ndatabases\n Postgres databases the built-in `database.fast-forward` operation can\n move forward, by name: `engine` (`postgres`), `url` and `as_of` (when\n the dump was captured, UTC with a `Z`). `url` is exactly one whole-string\n `${env.NAME}` reference \u2014 nothing before or after it \u2014 resolved inside\n the sandbox against the env actions get, including repository secrets.\n See the `data-fast-forward` topic.\n databases:\n app:\n engine: postgres\n url: ${env.DATABASE_URL}\n as_of: 2026-09-30T00:00:00Z\n\nenvironments\n Named flavors of the same sandbox \u2014 different starting data and config.\n Required, must include `default`. See the `environments` topic for the\n full model (`extends`, seed steps, `${id.key}` references, `env`).\n environments:\n default:\n description: |\n Workspace \"Acme\", owned by the owner persona. No projects.\n seeds:\n - { id: acme, action: create-workspace, input: { name: Acme, as: owner } }\n\nMULTI-LINE TEXT\n\n YAML `|` keeps line breaks (use it for multi-paragraph `description`s and\n the `setup` script). YAML `>-` folds lines into one string with no\n trailing newline (use it for a single long sentence, such as a long\n action `description`).\n";
|
|
58
|
+
var definition_default = "Everything about a sandbox lives in one file: `cofactor.sandbox.yaml`, at the\nrepository root or under a definition directory for a monorepo (for pushed\nimages, wherever you keep it \u2014 see the `upload` topic). The examples below\nare fragments of one file for the guide's example app: a project tracker\nwith a `web` front end, an `api` server and Postgres.\n`cofactor sandbox validate` checks every field below.\n\n Required: `version`, `resources`, `ports`, `params`, `env`, `services`,\n `lifecycle` and `environments`; write `{}` for one you don't use (e.g.\n `params: {}`, `lifecycle: { prepare: [] }`). `personas`, `actions`, `emulation`, `object_storage`\n and `databases` may be left out.\n Names of ports, services, personas, actions, environments and plugins\n are lowercase letters, digits and `-` (up to 32 characters, starting\n with a letter); a seed step's `id` is lowercase letters, digits and `_`\n (it is used in `${<id>.<output>}`, so no `-`).\n\nversion\n Always `1`. The schema version of the file itself.\n\nresources\n The machine every sandbox of this application gets.\n resources: { vcpus: 2, memory_mib: 4096 }\n\nsetup\n (GitHub-delivered only.) A root shell script that installs the\n toolchain: packages, language runtimes, database clusters. Runs once per\n toolchain layer (keyed by this script's text plus `resources` and\n `ports` \u2014 see the `overview` topic).\n setup: |\n apt-get update -qq\n apt-get install -y -qq postgresql-16 nodejs npm jq curl >/dev/null\n\nports\n Named ports the app listens on, each becoming its own public URL. Several\n are normal \u2014 bundle web and API together so one sandbox serves both UI\n flows and API sequences. `access: public` gets a routable URL; `private`\n stays internal. At most 8 total, including the implicit `sync` port every\n sandbox has (so at most 7 of your own), the `emulation` port added\n automatically when `emulation` is declared, and `object-storage` when\n `object_storage` is \u2014 see the `ports` topic.\n ports:\n web: { port: 3000, access: public }\n api: { port: 3001, access: public }\n\nparams\n Per-sandbox values, filled in by `prepare`, referenced from `env` as\n `${params.name}`. Three shapes:\n - a plain string: a fixed value, same in every sandbox.\n - `{port: <name>}`: the sandbox's own public URL for that port.\n `{port: emulation}` is the emulator's public URL, allowed when\n `emulation` is declared (it is the `oidc` plugin's issuer).\n - `{secret: true}`: a random value made when the version's ready\n snapshot is built, so every sandbox of that version and environment\n shares it (e.g. a session-signing key); not unique per claim.\n params:\n web_url: { port: web }\n api_url: { port: api }\n session_secret: { secret: true }\n A port param may be `live: true`: read its resolved env at request/use time\n from `/var/lib/cofactor/live-env.json`, a JSON object of env-name -> value.\n A fast copy receives its URLs through one copy-values exec, which\n keeps this file current using atomic replacement;\n re-read when the file's inode or modification time changes. No private\n Cofactor package is needed by your app.\n Eligible copies with stored snapshot prepare results skip prepare and keep\n services running: lifecycle.prepare does NOT run. They retain baked secrets\n until the snapshot is rebuilt. A copy is eligible for the fast path only\n when every port param is live.\n Any non-live port param keeps the per-copy prepare and service restart.\n An env key mixing a live port param with a non-live param also requires\n prepare, as does an emulation plugin config that references a port param.\n A real third-party secret (a Stripe or OpenAI key) is not a param: see\n the `secrets` topic.\n\nenv\n Environment variables the services, prepare commands and actions run\n with. `${params.name}` is substituted with that param's value. An\n environment can add its own on top (see the `env` topic). A secret of\n the same name (see the `secrets` topic) overrides the `env` entry.\n (`${env.NAME}` is a different, narrower reference: it appears only as\n the whole `url` of a `databases` entry \u2014 see the `databases` field.)\n env:\n DATABASE_URL: postgres://app@127.0.0.1:5432/app\n PUBLIC_URL: ${params.web_url}\n API_URL: ${params.api_url}\n SESSION_SECRET: ${params.session_secret}\n\nservices\n Long-running processes. `cofactor-sync` is reserved: every sandbox runs\n it (the sync server, see the `local` topic). Restart a service by hand\n with `cofactor sandbox exec <id> -- cofactor-sandbox restart <name>`.\n services:\n db:\n run: exec sandbox/start-postgres.sh\n health: { command: pg_isready -h 127.0.0.1 }\n api:\n run: cd api && exec npm start\n health: { http: 3001, path: /health }\n after_build: true # don't start until lifecycle.build has produced its output\n reload_on_prepare: true # restarted by prepare, so it sees per-sandbox env\n web:\n run: cd web && exec npm run serve\n health: { http: 3000, path: / }\n after_build: true\n reload_on_prepare: true # reads PUBLIC_URL, API_URL and FEATURE_FLAGS\n\n `health` is either `{ command: <shell> }` (checked by exit code) or\n `{ http: <port>, path: </...> }`. `after_build` services start once\n `lifecycle.build` has run (GitHub-delivered); in a pushed image they start\n at boot, and `sync --build` restarts them after its build steps.\n\npersonas\n Named login identities declared here; each one's password is generated\n when the snapshot is built and shared by its copies (see the `personas` topic). Only `email` is declared\n in the definition.\n personas:\n owner: { email: owner@example.test }\n member: { email: member@example.test }\n viewer: { email: viewer@example.test }\n\nlifecycle\n build Ordered steps. GitHub-delivered: they produce the repo-base\n layer. Pushed image (already built): they run only on\n `cofactor sandbox sync <id> --build`, never at push or claim. Each has `name`, `run` (a shell command) and\n `inputs` (git pathspecs, relative to the definition's\n directory, or the image's WORKDIR) \u2014 a step reruns only when the blobs its `inputs`\n match changed, or an earlier step ran.\n lifecycle:\n build:\n - name: install\n run: npm ci\n inputs: [package-lock.json, \"**/package.json\"]\n\n prepare Shell scripts run every time a sandbox is prepared (a snapshot\n build, a legacy copy claim or an in-place move to a new commit), after services with\n `reload_on_prepare` have restarted with per-sandbox values. Each\n param is also available as `COFACTOR_PARAM_<NAME>`.\n lifecycle:\n prepare:\n - bash sandbox/personas.sh\n\nactions\n Typed scripts \u2014 the one way data gets into a sandbox (seeds, tests,\n `cofactor sandbox run`). See the `seeds-and-actions` topic for\n what makes a good one; the field shapes are:\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, description: Who creates it }\n output:\n workspace_id: { type: string }\n timeout: 60 # seconds, defaults to 60, max 1800\n add-member:\n description: Adds a persona to a workspace with a role.\n run: sandbox/actions/add-member.sh\n input:\n workspace_id: { type: string, required: true }\n persona: { type: persona, required: true, description: Who is added }\n role: { type: string, required: true, enum: [owner, member, viewer] }\n as: { type: persona, required: true, description: Who adds them }\n\n Input properties: `type` (string | number | integer | boolean |\n persona \u2014 a persona's name, checked against `personas`), `description`,\n `required`, `default`, `enum`, `sensitive` (masked wherever input is\n logged or returned).\n Output properties: `type`, `description`.\n\nemulation\n Plugin id \u2192 its config (often `{}`). Declaring any plugin here adds the\n implicit, public, port-80 `emulation` port automatically \u2014 you don't\n declare it yourself. See the `emulation` topic for what each plugin\n intercepts. Plugin ids: `aws`, `email`, `google-oauth`, `jira`, `slack`, `oidc`\n (a standard OIDC provider for the app to sign in against; see the\n `authentication` topic).\n emulation:\n email: {}\n\nobject_storage\n An emulated S3 or GCS server with these buckets in every sandbox, its\n standard client env given to everything that runs, and an implicit\n public `object-storage` port. See the `object-storage` topic.\n object_storage: { provider: s3, buckets: [attachments] }\n\ndatabases\n Postgres databases the built-in `database.fast-forward` operation can\n move forward, by name: `engine` (`postgres`), `url` and `as_of` (when\n the dump was captured, UTC with a `Z`). `url` is exactly one whole-string\n `${env.NAME}` reference \u2014 nothing before or after it \u2014 resolved inside\n the sandbox against the env actions get, including repository secrets.\n See the `data-fast-forward` topic.\n databases:\n app:\n engine: postgres\n url: ${env.DATABASE_URL}\n as_of: 2026-09-30T00:00:00Z\n\nenvironments\n Named flavors of the same sandbox \u2014 different starting data and config.\n Required, must include `default`. See the `environments` topic for the\n full model (`extends`, seed steps, `${id.key}` references, `env`).\n environments:\n default:\n description: |\n Workspace \"Acme\", owned by the owner persona. No projects.\n seeds:\n - { id: acme, action: create-workspace, input: { name: Acme, as: owner } }\n\nMULTI-LINE TEXT\n\n YAML `|` keeps line breaks (use it for multi-paragraph `description`s and\n the `setup` script). YAML `>-` folds lines into one string with no\n trailing newline (use it for a single long sentence, such as a long\n action `description`).\n";
|
|
59
59
|
|
|
60
60
|
// ../content/src/sandbox-v2-guide/environments.txt
|
|
61
61
|
var environments_default = "An environment (a \"flavor\") is the same sandbox \u2014 same code, same toolchain\n\u2014 started with different data and config. `default` is required; every\ndefinition must declare it. Previews use `default`, and so does `cofactor\nsandbox claim` when you don't pass `--environment`.\n\nWHEN TO ADD A FLAVOR VS. PASSING INPUTS IN THE TEST\n\n Rule of thumb: add a flavor when many tests need the same starting state.\n Call actions from inside the test when only that test needs the data.\n\n In the example app, `default` seeds one workspace with an owner, a\n member and a viewer, because nearly every test signs in to a workspace\n as one of those roles. A test about archiving a project, on the other\n hand, calls `create-project` itself (see the worked example in\n `seeds-and-actions`) rather than getting a new flavor. If you find\n yourself writing a flavor whose seeds only exist to serve one test,\n that data belongs in the test's own action calls instead.\n\n Add a second flavor only when real tests need a starting state `default`\n can't give them cheaply \u2014 typically a different plan or set of feature\n flags (see \"WHAT TO BUILD FIRST\" in `seeds-and-actions`). Don't add one\n speculatively.\n\nDESCRIPTIONS ARE FOR THE READER\n\n `description` is required and can be multi-line (YAML `|`). Write it for\n the agent choosing between environments \u2014 say what's already there and\n who can do what, not just a label:\n environments:\n default:\n description: |\n Workspace \"Acme\": owner is its owner, member a member,\n viewer a read-only viewer. No projects yet; create them\n with create-project.\n\n A vague description (\"the default setup\") gives an agent nothing to\n decide with. Say what exists, in terms of the ids and roles a test will\n actually use.\n\nEXTENDS\n\n `extends: <other-environment-name>` composes one environment on top of\n another. The child's snapshot runs the **parent's seeds first, then the\n child's own**, in that order. `env` merges the same way: parent's keys\n first, the child's last (so a child can override a parent's value).\n Chains can be more than two deep; a cycle is rejected by `validate`.\n\n Step ids must be **unique across the whole resolved chain** \u2014 a parent\n and its child can't reuse the same id, since the child's steps can\n reference the parent's outputs.\n\n An environment carries its own `description`, `extends`, `seeds` and\n `env`. It has no `params` or `personas` of its own: those belong to the\n definition and are the same in every environment.\n\nSEED STEPS AND `${id.key}`\n\n Each `seeds` step has an `id`, exactly one of `action` or `operation`,\n and an `input`:\n - `id`: unique within the resolved chain; later steps reference this\n step's output as `${id.key}`.\n - `action`: a declared action's slug, or `<plugin>.<op>` for an\n emulator operation (e.g. `email.sent-emails`).\n - `operation`: a platform built-in, today only `database.fast-forward`\n (see `data-fast-forward` and `seeds-and-actions`).\n - `input`: literal values, or `${id.key}` references to an earlier\n step's output.\n\n A reference that is the **entire** value keeps the output's real type:\n input: { workspace_id: \"${acme.workspace_id}\" } # exactly acme's workspace_id\n\n A reference **embedded** in a longer string becomes a string\n interpolation instead:\n input: { name: \"Board for ${acme.workspace_id}\" } # becomes a string\n\n `${id.key}` must point at a step that ran earlier in the chain and at one\n of that step's *declared* output keys \u2014 `cofactor sandbox validate`\n checks both and reports the path.\n\n The example app's environments, in full:\n environments:\n default:\n description: |\n Workspace \"Acme\": owner is its owner, member a member,\n viewer a read-only viewer. No projects yet.\n seeds:\n - id: acme\n action: create-workspace\n input: { name: Acme, as: owner }\n - id: add_member\n action: add-member\n input: { workspace_id: \"${acme.workspace_id}\", persona: member, role: member, as: owner }\n - id: add_viewer\n action: add-member\n input: { workspace_id: \"${acme.workspace_id}\", persona: viewer, role: viewer, as: owner }\n free-plan:\n extends: default\n description: Same as default, but Acme is moved to the Free plan (3-project limit).\n seeds:\n - id: downgrade\n action: set-plan\n input: { workspace_id: \"${acme.workspace_id}\", plan: free }\n beta-features:\n extends: default\n description: Same as default, with the timeline view turned on.\n env:\n FEATURE_FLAGS: timeline\n\n `add_member` couldn't run before `acme` \u2014 its `workspace_id` input\n depends on `acme`'s output. Seeds always run in the order they're\n written. `free-plan` sets its plan with a seed action because the app\n stores plans in its database; `beta-features` uses `env` because the\n app reads its flags from an environment variable (see the `env` topic\n for which processes see it).\n";
|
|
@@ -70,7 +70,7 @@ var data_fast_forward_default = "A database dump keeps the dates of the day it w
|
|
|
70
70
|
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 preview` for PR\n previews. They are never printed in a PR comment \u2014 a PR comment only\n links to where they can be looked up, keeping generated passwords out of\n anything that ends up in GitHub's own history. Cofactor's test runs sign\n in with them automatically, as the application's persona of the same\n 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";
|
|
71
71
|
|
|
72
72
|
// ../content/src/sandbox-v2-guide/authentication.txt
|
|
73
|
-
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` and `lifecycle.build` are for GitHub-delivered definitions, as in the\n `definition` topic; a pushed image omits both (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";
|
|
73
|
+
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";
|
|
74
74
|
|
|
75
75
|
// ../content/src/sandbox-v2-guide/env.txt
|
|
76
76
|
var env_default = "An environment's `env` block (under `environments.<name>.env`) overrides\nenvironment variables for exactly three things:\n\n - running services that declare `reload_on_prepare: true` \u2014 they are\n restarted by `prepare` (and by an in-place move to a new commit), so\n they always see the current chain's merged env;\n - `lifecycle.prepare` commands, run fresh on every prepare;\n - actions, which run with the prepare env plus that call's\n `INPUT_<NAME>` values.\n\n environments:\n beta-features:\n extends: default\n description: Same as default, with the timeline view turned on.\n env:\n FEATURE_FLAGS: timeline\n\nIt does NOT reach:\n\n - `lifecycle.build` steps. Build steps run once into the repo-base layer,\n before any environment is chosen, so an environment's `env` does not\n exist at build time. A value your build bakes into its output (e.g. a\n front-end bundle's API URL or feature flags) can't vary by\n environment: read it at serve time instead, or set it with a seed\n action.\n - services that don't declare `reload_on_prepare`. A service without it\n only ever sees the definition's top-level `env` and layer-time param\n values (from when its layer was built) \u2014 it is not restarted when a\n sandbox is prepared, so a per-environment override would never take\n effect for it.\n\nAn environment's `env` values are used as written: `${params.name}` is\nsubstituted only in the definition's top-level `env`. To vary a value per\nenvironment that must not live in the repository (an API key), use a\nsecret scoped to that environment: see the `secrets` topic.\n\n`env` MERGE ORDER\n\n Composition follows the same parent-then-child rule as seeds (see\n `environments`): the resolved chain's env is the parent's `env` merged\n first, then the child's own, so a child environment can override a\n specific key its parent sets without having to repeat the rest. Secrets\n (see the `secrets` topic) merge last, above every `env`.\n\nRELATION TO THE DEFINITION'S TOP-LEVEL `env`\n\n The definition's top-level `env` (outside any environment) is the base:\n fixed values and `${params.name}` substitutions that apply regardless of\n which environment is claimed \u2014 `DATABASE_URL`, `PUBLIC_URL` and so on in\n the example app. An environment's `env` is layered on top of that base\n for the reachable surfaces above, not a replacement for it.\n";
|
|
@@ -91,16 +91,16 @@ var ports_default = "A sandbox can declare several ports, and each one gets its
|
|
|
91
91
|
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";
|
|
92
92
|
|
|
93
93
|
// ../content/src/sandbox-v2-guide/cli.txt
|
|
94
|
-
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`, no `lifecycle.build`);\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. An environment kept warm switches to the new head\n once its warm-up finishes (staff-managed, see `warm`). `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 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` (GitHub\n applications) reruns the changed build steps and restarts `after_build`\n services. `--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 flow's environment must be the sandbox's application and\n environment (`--environment`). The run records the version and synced\n 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 workflow guide prove-changes`.\n\ncofactor sandbox warm list | set | show | clear\n Staff only: keep an environment warm so claims start in about a second.\n `warm set --application A --environment E --slots N [--tag T]`, then\n `warm show` (Filling / On / Switching / Needs attention) with the same\n arguments, `warm list` for the whole stage, `warm clear` to turn it off.\n See the `warm` topic.\n\ncofactor sandbox guide v2 [topic]\n Prints this guide. Omit `topic` to print every section; name one (e.g.\n `upload`) to print just that topic.\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";
|
|
94
|
+
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. An environment kept warm switches to the new head\n once its warm-up finishes (staff-managed, see `warm`). `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 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 flow's environment must be the sandbox's application and\n environment (`--environment`). The run records the version and synced\n 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 warm list | set | show | clear\n Staff only: keep an environment warm so claims start in about a second.\n `warm set --application A --environment E --slots N [--tag T]`, then\n `warm show` (Filling / On / Switching / Needs attention) with the same\n arguments, `warm list` for the whole stage, `warm clear` to turn it off.\n See the `warm` topic.\n\ncofactor sandbox guide v2 [topic]\n Prints this guide. Omit `topic` to print every section; name one (e.g.\n `upload`) to print just that topic.\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";
|
|
95
95
|
|
|
96
96
|
// ../content/src/sandbox-v2-guide/upload.txt
|
|
97
|
-
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. There is no build step in a pushed sandbox: everything\n the app needs (dependencies, compiled assets, a database server if it\n uses one) is in the image.\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:\n docker buildx build --platform linux/amd64 --load -t app:sandbox \\\n --cache-from type=gha --cache-to type=gha,mode=max .\n (or `type=registry` with a cache image of your own). In a plain\n `run:` step buildx can\'t see the Actions cache token and skips\n `type=gha` without a warning: add\n `uses: crazy-max/ghaction-github-runtime@v3` before that step, or\n build with `docker/build-push-action` (same cache-from and cache-to).\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 jobs:\n nightly:\n runs-on: ubuntu-latest\n timeout-minutes: 180\n env:\n COFACTOR_API_TOKEN: ${{ secrets.COFACTOR_API_TOKEN }}\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@v3\n - uses: crazy-max/ghaction-github-runtime@v3\n - name: Build the image\n run: |\n docker buildx build --platform linux/amd64 --load \\\n -t acme-tracker:sandbox \\\n --cache-from type=gha --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 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 run: cofactor test-suite run --suite "Acme Tracker nightly"\n\n The CLI reads `COFACTOR_API_TOKEN` from the environment (no login step).\n The cache flags and the ghaction-github-runtime step are the ones under FAST\n RE-PUSHES. `--wait` follows the import for up to 50 minutes and fails the\n job if 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 (there is no build step):\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 everything about building: no `setup`, no\n `lifecycle.build`, no `after_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';
|
|
97
|
+
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:\n docker buildx build --platform linux/amd64 --load -t app:sandbox \\\n --cache-from type=gha --cache-to type=gha,mode=max .\n (or `type=registry` with a cache image of your own). In a plain\n `run:` step buildx can\'t see the Actions cache token and skips\n `type=gha` without a warning: add\n `uses: crazy-max/ghaction-github-runtime@v3` before that step, or\n build with `docker/build-push-action` (same cache-from and cache-to).\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 jobs:\n nightly:\n runs-on: ubuntu-latest\n timeout-minutes: 180\n env:\n COFACTOR_API_TOKEN: ${{ secrets.COFACTOR_API_TOKEN }}\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@v3\n - uses: crazy-max/ghaction-github-runtime@v3\n - name: Build the image\n run: |\n docker buildx build --platform linux/amd64 --load \\\n -t acme-tracker:sandbox \\\n --cache-from type=gha --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 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 run: cofactor test-suite run --suite "Acme Tracker nightly"\n\n The CLI reads `COFACTOR_API_TOKEN` from the environment (no login step).\n The cache flags and the ghaction-github-runtime step are the ones under FAST\n RE-PUSHES. `--wait` follows the import for up to 50 minutes and fails the\n job if 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';
|
|
98
98
|
|
|
99
99
|
// ../content/src/sandbox-v2-guide/warm.txt
|
|
100
100
|
var warm_default = "KEEPING AN ENVIRONMENT WARM (STAFF ONLY)\n\nA claim normally copies the environment's ready snapshot onto a worker that\nmay not hold it yet, and fetching it first takes a while. Keep warm makes\nsure some workers already hold the snapshot and have room to spare, so a\nclaim starts in about a second. Nothing is pre-booted: no sandbox runs until\nsomeone claims one. What is kept ready is the snapshot on the workers and\nfree slots to start copies in.\n\nOnly a superadmin can turn it on, because it holds capacity on the sandbox\nfleet. Everyone else just sees faster claims.\n\n cofactor sandbox warm set --application A --environment E --slots N [--tag T]\n cofactor sandbox warm show --application A --environment E\n cofactor sandbox warm list\n cofactor sandbox warm clear --application A --environment E\n\nThese act in the org your session is signed in to (`--org` picks which of\nyour sessions, as for every command). To act in another org, pass\n`--target-org <id>` and give the application by id. `--json` prints the\nraw data; `--env` stays the stage (dev / prod).\n\nSLOTS\n\n`--slots N` (1 to 12) is how many free slots to keep ready for this\nenvironment, one slot being what a claim of this environment takes (its\ndefinition's vCPUs and memory). When claims use some, Cofactor brings up or\nreuses another worker and puts the snapshot on it before the free slots run\nout. A rule of thumb for an environment of 2 vCPUs and 4 GiB: a worker has\nabout 3 slots with SMT off and about 6 with SMT on. Read the real figure for\nthe fleet you are on from the worker (`blackcat fleet nodes`), not from this\npage, and keep `N` at or below what one worker holds so a single extra\nworker is enough.\n\nCOST\n\nCost follows total headroom slots, and a warm worker is shared by every\nwarm environment: two environments at 2 slots each on the same worker cost\nabout what one does. Workers holding warm slots are not retired by idle\nscale-in, so at least one worker stays up while any environment is kept warm.\nClear what nobody needs.\n\nWarm needs the Blackcat release that supports it on the placer, the gateway\nand every worker, and every worker must report a cache budget before warm is\nenabled.\n\nTHE TAG\n\nA repository application has no tag: the snapshot kept warm is the default\nbranch's. A pushed application has no default branch, so `--tag` is required\nand names the push to keep warm (for example `nightly`). Giving a tag to a\nrepository application, or leaving it off a pushed one, is refused.\n\nWHAT CLAIMS SEE\n\nWhile an environment is kept warm, claims follow the warm snapshot, not the\nnewest one. When main (or the tag) moves to a new snapshot, Cofactor builds\nit, warms it on workers beside the old one, and only then moves claims over.\nSo a claim, a workflow run and `envs` can lag main by the 2 to 3 minutes it\ntakes to warm the new snapshot. If you change the tag, claims naming the new\ntag keep getting the push warmed for the old tag until the switch finishes.\n\nREADING `warm show`\n\n Keep warm: On (2 slots)\n Serving claims: 3f9a1c (2 warm slots free now)\n\nState is one of:\n\n Filling the first snapshot is being warmed; claims are not warm yet\n On claims start from a worker that holds the snapshot\n Switching a newer snapshot is being warmed; claims still use the\n old one, which stays warm until the new one is ready\n Needs attention warming failed and nothing is serving; `Error` says why\n\n\"Serving claims\" is the snapshot claims use, and the free warm slots are read\nlive from the fleet: On with fewer than N free means the fleet is topping up.\n\"Warming next\" appears while Switching. While Switching, `Last error` means\nthe new snapshot could not be warmed (for example its seed failed): the old\none keeps serving and the switch is retried. The CLI shows these labels; the\nJSON carries `warming`, `warm`, `switching` and `failed`.\n\nTURNING IT OFF, AND ROLLING BACK THE FLEET\n\n`warm clear` releases the environment's capacity and removes the setting;\nthe extra worker retires when the fleet's idle scale-in allows. `warm clear`\nreleases the reservations itself, and if a release fails it fails and keeps\nthe row, so an empty `warm list` means every warm reservation was released.\n\nDo not roll the sandbox fleet's placer back while any environment is kept\nwarm: an older placer would hold the slots instead of keeping them soft.\nFirst:\n\n 1. `cofactor sandbox warm list --env <stage>` lists every environment kept\n warm on the stage.\n 2. `warm clear` each one.\n 3. Confirm every reservation id the list showed now reports `released`\n (`blackcat reservation get <id>`), or that `warm list` is empty.\n 4. Only then roll the placer back.\n";
|
|
101
101
|
|
|
102
102
|
// ../content/src/sandbox-v2-guide/local.txt
|
|
103
|
-
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
|
|
103
|
+
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 flow's environment must be the sandbox's application\n and environment (otherwise the run is refused: pass --environment), 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";
|
|
104
104
|
|
|
105
105
|
// ../content/src/sandbox-v2-guide/index.ts
|
|
106
106
|
var SANDBOX_V2_GUIDE_SECTIONS = [
|
|
@@ -644,6 +644,8 @@ function emulationReferenceIssues(emulation, params) {
|
|
|
644
644
|
// ../sandbox-helper/src/manifest.ts
|
|
645
645
|
var EMULATION_PORT_NAME = "emulation";
|
|
646
646
|
var DEFINITION_FILE = "cofactor.sandbox.yaml";
|
|
647
|
+
var PLAYWRIGHT_NODE_VERSION = "v22.16.0";
|
|
648
|
+
var PLAYWRIGHT_NODE_BINARY = `/opt/cofactor/external-tests/node-${PLAYWRIGHT_NODE_VERSION}-linux-x64/bin/node`;
|
|
647
649
|
var IMAGE_DIR = "/cofactor";
|
|
648
650
|
var IMAGE_DEFINITION_FILE = `${IMAGE_DIR}/${DEFINITION_FILE}`;
|
|
649
651
|
var IMAGE_CONFIG_FILE = `${IMAGE_DIR}/image.json`;
|
|
@@ -870,7 +872,7 @@ var sandboxAuthResultSchema = z2.strictObject({
|
|
|
870
872
|
var SANDBOX_AUTH_MAX_BYTES = 1024 * 1024;
|
|
871
873
|
var definitionSchema = z2.strictObject({
|
|
872
874
|
...sharedFields,
|
|
873
|
-
lifecycle: z2.strictObject({ prepare: prepareCommands })
|
|
875
|
+
lifecycle: z2.strictObject({ prepare: prepareCommands, build: z2.array(buildStep).min(1).optional() })
|
|
874
876
|
});
|
|
875
877
|
var githubDefinitionSchema = definitionSchema.extend({
|
|
876
878
|
/**
|
|
@@ -1190,17 +1192,6 @@ function crossCheck(data, delivery, issues) {
|
|
|
1190
1192
|
if ("setup" in data) {
|
|
1191
1193
|
issues.push({ path: "setup", message: "an uploaded image is already built: remove setup (install it in the image)" });
|
|
1192
1194
|
}
|
|
1193
|
-
if (isRecord2(data.lifecycle) && "build" in data.lifecycle) {
|
|
1194
|
-
issues.push({
|
|
1195
|
-
path: "lifecycle.build",
|
|
1196
|
-
message: "an uploaded image is already built: remove lifecycle.build (build it in the image)"
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
for (const [name, spec] of Object.entries(services)) {
|
|
1200
|
-
if (isRecord2(spec) && spec.after_build === true) {
|
|
1201
|
-
issues.push({ path: `services.${name}.after_build`, message: "an uploaded image has no build to wait for" });
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
1195
|
}
|
|
1205
1196
|
const params = isRecord2(data.params) ? data.params : {};
|
|
1206
1197
|
for (const [name, value] of Object.entries(params)) {
|
|
@@ -2795,9 +2786,6 @@ ${SYNC_USAGE}`);
|
|
|
2795
2786
|
);
|
|
2796
2787
|
if (!sessionRes.ok) return fail4(sessionRes.error ?? "could not start a sync");
|
|
2797
2788
|
const session = sessionRes.data;
|
|
2798
|
-
if (build && session.delivery === "upload") {
|
|
2799
|
-
return fail4("a pushed image has no build to rerun: sync, then run your build with `cofactor sandbox exec <id> -- <command>`.");
|
|
2800
|
-
}
|
|
2801
2789
|
try {
|
|
2802
2790
|
let local;
|
|
2803
2791
|
let decide;
|
|
@@ -3377,7 +3365,7 @@ function printHelp() {
|
|
|
3377
3365
|
" was once tracked at that path; everything else (node_modules/, build output) is kept. Each sync extends the",
|
|
3378
3366
|
" sandbox's lease, up to 8 h after the claim. Needs the `sandbox:exec` scope for API keys.",
|
|
3379
3367
|
" Root: the repository (GitHub) or the directory holding cofactor.sandbox.yaml (pushed: its WORKDIR mirrors it).",
|
|
3380
|
-
" --build
|
|
3368
|
+
" --build Rerun the definition's build steps whose inputs changed and restart after_build services",
|
|
3381
3369
|
" --to <path> With <local-dir>: mirror it into <path> under the sync root, ignore rules aside",
|
|
3382
3370
|
" (e.g. `bun run build`, then `sync <id> frontend/dist --to static`)",
|
|
3383
3371
|
" Nothing restarts on its own: `cofactor sandbox exec <id> -- cofactor-sandbox restart <service>`.",
|
|
@@ -5487,4 +5475,4 @@ export {
|
|
|
5487
5475
|
withPluginEnabled,
|
|
5488
5476
|
withoutPlugin
|
|
5489
5477
|
};
|
|
5490
|
-
//# sourceMappingURL=sandbox-
|
|
5478
|
+
//# sourceMappingURL=sandbox-TJQPMRGS.js.map
|